niceeval 0.9.0 → 0.10.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 (428) hide show
  1. package/INDEX.md +31 -33
  2. package/dist/agents/types.d.ts +20 -17
  3. package/dist/context/types.d.ts +21 -21
  4. package/dist/i18n/en.d.ts +10 -2
  5. package/dist/i18n/zh-CN.d.ts +15 -7
  6. package/dist/o11y/execution-tree.d.ts +103 -0
  7. package/dist/o11y/otlp/select.d.ts +22 -0
  8. package/dist/report/{react → assets}/colors.d.ts +8 -0
  9. package/dist/report/{react → assets}/colors.js +23 -0
  10. package/dist/report/built-in/index.d.ts +2 -2
  11. package/dist/report/built-in/index.js +2 -2
  12. package/dist/report/built-in/standard.d.ts +8 -1
  13. package/dist/report/built-in/standard.js +17 -7
  14. package/dist/report/components/attempt-detail/AttemptAssertions.d.ts +6 -0
  15. package/dist/report/components/attempt-detail/AttemptAssertions.js +17 -0
  16. package/dist/report/components/attempt-detail/AttemptConversation.d.ts +6 -0
  17. package/dist/report/components/attempt-detail/AttemptConversation.js +34 -0
  18. package/dist/report/components/attempt-detail/AttemptDiagnostics.d.ts +6 -0
  19. package/dist/report/components/attempt-detail/AttemptDiagnostics.js +7 -0
  20. package/dist/report/components/attempt-detail/AttemptDiff.d.ts +6 -0
  21. package/dist/report/components/attempt-detail/AttemptDiff.js +11 -0
  22. package/dist/report/components/attempt-detail/AttemptError.d.ts +6 -0
  23. package/dist/report/components/attempt-detail/AttemptError.js +16 -0
  24. package/dist/report/components/attempt-detail/AttemptFixPrompt.d.ts +6 -0
  25. package/dist/report/components/attempt-detail/AttemptFixPrompt.js +7 -0
  26. package/dist/report/components/attempt-detail/AttemptSource.d.ts +6 -0
  27. package/dist/report/components/attempt-detail/AttemptSource.js +27 -0
  28. package/dist/report/components/attempt-detail/AttemptSummary.d.ts +8 -0
  29. package/dist/report/components/attempt-detail/AttemptSummary.js +20 -0
  30. package/dist/report/components/attempt-detail/AttemptTimeline.d.ts +6 -0
  31. package/dist/report/components/attempt-detail/AttemptTimeline.js +28 -0
  32. package/dist/report/components/attempt-detail/AttemptTrace.d.ts +6 -0
  33. package/dist/report/components/attempt-detail/AttemptTrace.js +29 -0
  34. package/dist/report/components/attempt-detail/AttemptUsage.d.ts +6 -0
  35. package/dist/report/components/attempt-detail/AttemptUsage.js +20 -0
  36. package/dist/report/components/attempt-detail/compute.d.ts +24 -0
  37. package/dist/report/components/attempt-detail/compute.js +254 -0
  38. package/dist/report/components/attempt-detail/faces.d.ts +14 -0
  39. package/dist/report/components/attempt-detail/faces.js +235 -0
  40. package/dist/report/components/attempt-detail/index.d.ts +38 -0
  41. package/dist/report/components/attempt-detail/index.js +527 -0
  42. package/dist/report/{react → components}/cell.d.ts +5 -3
  43. package/dist/report/{react → components}/cell.js +3 -3
  44. package/dist/report/{react → components/entity-lists}/AttemptList.d.ts +8 -4
  45. package/dist/report/{react → components/entity-lists}/AttemptList.js +16 -10
  46. package/dist/report/{react → components/entity-lists}/EvalList.d.ts +3 -3
  47. package/dist/report/{react → components/entity-lists}/EvalList.js +0 -0
  48. package/dist/report/{react → components/entity-lists}/ExperimentList.d.ts +3 -3
  49. package/dist/report/{react → components/entity-lists}/ExperimentList.js +10 -10
  50. package/dist/report/components/entity-lists/compute.d.ts +23 -0
  51. package/dist/report/components/entity-lists/compute.js +171 -0
  52. package/dist/report/components/entity-lists/faces.d.ts +5 -0
  53. package/dist/report/components/entity-lists/faces.js +174 -0
  54. package/dist/report/components/entity-lists/index.d.ts +51 -0
  55. package/dist/report/components/entity-lists/index.js +165 -0
  56. package/dist/report/{react → components}/fixtures.d.ts +2 -2
  57. package/dist/report/{react → components}/fixtures.js +1 -1
  58. package/dist/report/{react → components/metric-views}/DeltaTable.d.ts +2 -2
  59. package/dist/report/{react → components/metric-views}/DeltaTable.js +4 -4
  60. package/dist/report/{react → components/metric-views}/MetricBars.d.ts +3 -3
  61. package/dist/report/{react → components/metric-views}/MetricBars.js +3 -3
  62. package/dist/report/{react → components/metric-views}/MetricLine.d.ts +2 -2
  63. package/dist/report/{react → components/metric-views}/MetricLine.js +8 -5
  64. package/dist/report/{react → components/metric-views}/MetricMatrix.d.ts +3 -3
  65. package/dist/report/{react → components/metric-views}/MetricMatrix.js +4 -4
  66. package/dist/report/components/metric-views/MetricScatter.d.ts +11 -0
  67. package/dist/report/{react → components/metric-views}/MetricScatter.js +26 -23
  68. package/dist/report/{react → components/metric-views}/MetricTable.d.ts +3 -3
  69. package/dist/report/{react → components/metric-views}/MetricTable.js +4 -4
  70. package/dist/report/{react → components/metric-views}/Scoreboard.d.ts +2 -2
  71. package/dist/report/{react → components/metric-views}/Scoreboard.js +3 -3
  72. package/dist/report/components/metric-views/compute.d.ts +89 -0
  73. package/dist/report/{compute.js → components/metric-views/compute.js} +32 -415
  74. package/dist/report/components/metric-views/faces.d.ts +13 -0
  75. package/dist/report/components/metric-views/faces.js +381 -0
  76. package/dist/report/components/metric-views/index.d.ts +50 -0
  77. package/dist/report/components/metric-views/index.js +273 -0
  78. package/dist/report/{text → components/metric-views}/plot.js +1 -1
  79. package/dist/report/components/shared-compute.d.ts +22 -0
  80. package/dist/report/components/shared-compute.js +47 -0
  81. package/dist/report/components/shared-faces.d.ts +9 -0
  82. package/dist/report/components/shared-faces.js +26 -0
  83. package/dist/report/components/shared.d.ts +68 -0
  84. package/dist/report/components/shared.js +125 -0
  85. package/dist/report/{react → components/site-components}/CopyFixPrompt.d.ts +2 -2
  86. package/dist/report/{react → components/site-components}/CopyFixPrompt.js +2 -2
  87. package/dist/report/{react → components/site-components}/HeroCard.d.ts +2 -2
  88. package/dist/report/{react → components/site-components}/HeroCard.js +2 -2
  89. package/dist/report/{react → components/site-components}/ScopeWarnings.d.ts +2 -2
  90. package/dist/report/{react → components/site-components}/ScopeWarnings.js +3 -3
  91. package/dist/report/{react → components/site-components}/TraceWaterfall.d.ts +3 -3
  92. package/dist/report/{react → components/site-components}/TraceWaterfall.js +5 -5
  93. package/dist/report/components/site-components/compute.d.ts +28 -0
  94. package/dist/report/components/site-components/compute.js +132 -0
  95. package/dist/report/components/site-components/faces.d.ts +21 -0
  96. package/dist/report/components/site-components/faces.js +75 -0
  97. package/dist/report/components/site-components/index.d.ts +74 -0
  98. package/dist/report/components/site-components/index.js +220 -0
  99. package/dist/report/{scope-warnings.d.ts → components/site-components/scope-warnings.d.ts} +2 -2
  100. package/dist/report/{scope-warnings.js → components/site-components/scope-warnings.js} +2 -2
  101. package/dist/report/{react → components/summaries}/ScopeSummary.d.ts +2 -2
  102. package/dist/report/{react → components/summaries}/ScopeSummary.js +17 -8
  103. package/dist/report/components/summaries/compute.d.ts +7 -0
  104. package/dist/report/components/summaries/compute.js +51 -0
  105. package/dist/report/components/summaries/faces.d.ts +7 -0
  106. package/dist/report/components/summaries/faces.js +38 -0
  107. package/dist/report/components/summaries/index.d.ts +27 -0
  108. package/dist/report/components/summaries/index.js +78 -0
  109. package/dist/report/definition/grid-layout.d.ts +50 -0
  110. package/dist/report/definition/grid-layout.js +89 -0
  111. package/dist/report/{primitives.d.ts → definition/primitives.d.ts} +34 -4
  112. package/dist/report/{primitives.js → definition/primitives.js} +101 -10
  113. package/dist/report/{report.d.ts → definition/report.d.ts} +36 -65
  114. package/dist/report/{report.js → definition/report.js} +58 -90
  115. package/dist/report/{text/table.d.ts → definition/table-text.d.ts} +2 -2
  116. package/dist/report/{text/table.js → definition/table-text.js} +2 -2
  117. package/dist/report/{tree.d.ts → definition/tree.d.ts} +44 -12
  118. package/dist/report/{tree.js → definition/tree.js} +10 -12
  119. package/dist/report/index.d.ts +33 -18
  120. package/dist/report/index.js +23 -14
  121. package/dist/report/{aggregate.d.ts → model/aggregate.d.ts} +12 -4
  122. package/dist/report/{aggregate.js → model/aggregate.js} +28 -8
  123. package/dist/report/{flag.d.ts → model/flag.d.ts} +14 -1
  124. package/dist/report/{flag.js → model/flag.js} +35 -0
  125. package/dist/report/{format.d.ts → model/format.d.ts} +11 -5
  126. package/dist/report/{format.js → model/format.js} +48 -4
  127. package/dist/report/{locale.d.ts → model/locale.d.ts} +17 -17
  128. package/dist/report/{locale.js → model/locale.js} +31 -33
  129. package/dist/report/{metrics.d.ts → model/metrics.d.ts} +2 -2
  130. package/dist/report/{metrics.js → model/metrics.js} +2 -2
  131. package/dist/report/{types.d.ts → model/types.d.ts} +140 -20
  132. package/dist/report/react/index.d.ts +32 -21
  133. package/dist/report/react/index.js +33 -21
  134. package/dist/report/{load.d.ts → runtime/load.d.ts} +1 -1
  135. package/dist/report/{load.js → runtime/load.js} +1 -1
  136. package/dist/report/runtime/text.d.ts +67 -0
  137. package/dist/report/runtime/text.js +114 -0
  138. package/dist/report/runtime/web.d.ts +35 -0
  139. package/dist/report/runtime/web.js +70 -0
  140. package/dist/results/annotated-source.d.ts +87 -0
  141. package/dist/results/attempt-evidence.d.ts +74 -0
  142. package/dist/results/attempt-source.d.ts +15 -0
  143. package/dist/results/select.d.ts +12 -2
  144. package/dist/results/select.js +27 -5
  145. package/dist/runner/eval-selection.d.ts +26 -0
  146. package/dist/runner/feedback/sink.d.ts +26 -1
  147. package/dist/runner/types.d.ts +149 -23
  148. package/dist/sandbox/docker.d.ts +6 -0
  149. package/dist/sandbox/types.d.ts +23 -21
  150. package/dist/scoring/display.d.ts +7 -2
  151. package/dist/scoring/display.js +42 -4
  152. package/dist/scoring/types.d.ts +3 -3
  153. package/dist/shared/aggregate.d.ts +15 -1
  154. package/dist/shared/aggregate.js +32 -1
  155. package/dist/shared/types.d.ts +6 -1
  156. package/docs-site/images/logo-dark.svg +7 -0
  157. package/docs-site/images/logo.svg +6 -5
  158. package/docs-site/zh/README.md +8 -7
  159. package/docs-site/zh/examples/ai-agent-application.mdx +5 -5
  160. package/docs-site/zh/examples/coding-agent-extensions.mdx +4 -4
  161. package/docs-site/zh/examples/index.mdx +3 -3
  162. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +1 -1
  163. package/docs-site/zh/examples/integrations/claude-sdk.mdx +3 -3
  164. package/docs-site/zh/examples/integrations/codex-sdk.mdx +3 -3
  165. package/docs-site/zh/examples/integrations/langgraph.mdx +3 -3
  166. package/docs-site/zh/examples/integrations/pi-sdk.mdx +2 -2
  167. package/docs-site/zh/explanation/adapter.mdx +19 -19
  168. package/docs-site/zh/explanation/assert.mdx +9 -9
  169. package/docs-site/zh/explanation/drive.mdx +4 -4
  170. package/docs-site/zh/explanation/evals.mdx +8 -8
  171. package/docs-site/zh/explanation/experiment.mdx +8 -8
  172. package/docs-site/zh/explanation/hitl.mdx +13 -13
  173. package/docs-site/zh/explanation/judge.mdx +3 -3
  174. package/docs-site/zh/explanation/overview.mdx +7 -7
  175. package/docs-site/zh/explanation/runner.mdx +11 -11
  176. package/docs-site/zh/explanation/tier.mdx +6 -6
  177. package/docs-site/zh/index.mdx +14 -14
  178. package/docs-site/zh/introduction.mdx +13 -16
  179. package/docs-site/zh/reference/builtin-agents.mdx +61 -28
  180. package/docs-site/zh/reference/capabilities.mdx +7 -27
  181. package/docs-site/zh/reference/cli.mdx +35 -32
  182. package/docs-site/zh/reference/define-agent.mdx +32 -31
  183. package/docs-site/zh/reference/define-config.mdx +6 -6
  184. package/docs-site/zh/reference/define-eval.mdx +33 -20
  185. package/docs-site/zh/reference/events.mdx +4 -4
  186. package/docs-site/zh/reference/expect.mdx +3 -3
  187. package/docs-site/zh/reference/official-adapters.mdx +17 -17
  188. package/docs-site/zh/reference/report-components.mdx +193 -121
  189. package/docs-site/zh/reference/results-data.mdx +13 -13
  190. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +11 -11
  191. package/docs-site/zh/troubleshooting/debugging.mdx +43 -19
  192. package/docs-site/zh/{how-to → tutorials}/agent-feedback-loop.mdx +38 -38
  193. package/docs-site/zh/tutorials/agent-onboarding.mdx +100 -0
  194. package/docs-site/zh/tutorials/authoring.mdx +211 -0
  195. package/docs-site/zh/{how-to → tutorials}/ci-integration.mdx +8 -8
  196. package/docs-site/zh/{how-to → tutorials}/connect-otel.mdx +22 -22
  197. package/docs-site/zh/{how-to → tutorials}/connect-your-agent.mdx +58 -63
  198. package/docs-site/zh/tutorials/custom-reports.mdx +453 -0
  199. package/docs-site/zh/{how-to → tutorials}/dataset-fanout.mdx +4 -4
  200. package/docs-site/zh/tutorials/experiments.mdx +103 -0
  201. package/docs-site/zh/{how-to → tutorials}/fixtures.mdx +9 -9
  202. package/docs-site/zh/{how-to → tutorials}/publish-report.mdx +11 -5
  203. package/docs-site/zh/tutorials/quickstart.mdx +18 -18
  204. package/docs-site/zh/{how-to → tutorials}/reporters.mdx +6 -6
  205. package/docs-site/zh/{how-to → tutorials}/sandbox-agent.mdx +6 -6
  206. package/docs-site/zh/{how-to → tutorials}/sandbox-providers.mdx +41 -19
  207. package/docs-site/zh/{how-to → tutorials}/scoring-guide.mdx +4 -4
  208. package/docs-site/zh/{how-to → tutorials}/viewing-results.mdx +59 -50
  209. package/docs-site/zh/tutorials/write-experiment.mdx +355 -0
  210. package/docs-site/zh/{how-to → tutorials}/write-send.mdx +40 -40
  211. package/package.json +8 -4
  212. package/src/agents/bub.ts +21 -8
  213. package/src/agents/claude-code.ts +23 -10
  214. package/src/agents/codex.test.ts +22 -7
  215. package/src/agents/codex.ts +22 -9
  216. package/src/agents/index.ts +2 -2
  217. package/src/agents/openai-compat.ts +1 -1
  218. package/src/agents/post-setup.ts +45 -22
  219. package/src/agents/streaming.ts +1 -1
  220. package/src/agents/types.ts +20 -17
  221. package/src/agents/ui-message-stream.test.ts +10 -0
  222. package/src/agents/ui-message-stream.ts +12 -1
  223. package/src/cli.ts +77 -64
  224. package/src/context/types.ts +21 -21
  225. package/src/define.ts +13 -0
  226. package/src/i18n/en.ts +30 -15
  227. package/src/i18n/zh-CN.ts +30 -15
  228. package/src/index.ts +2 -0
  229. package/src/report/{react → assets}/colors.ts +22 -0
  230. package/src/report/{react → assets}/enhance.js +1 -33
  231. package/src/report/{react → assets}/styles.css +136 -112
  232. package/src/report/built-in/index.tsx +2 -2
  233. package/src/report/built-in/standard.tsx +18 -6
  234. package/src/report/components/attempt-detail/AttemptAssertions.tsx +67 -0
  235. package/src/report/components/attempt-detail/AttemptConversation.tsx +110 -0
  236. package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +38 -0
  237. package/src/report/components/attempt-detail/AttemptDiff.tsx +35 -0
  238. package/src/report/components/attempt-detail/AttemptError.tsx +31 -0
  239. package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +26 -0
  240. package/src/report/components/attempt-detail/AttemptSource.tsx +80 -0
  241. package/src/report/components/attempt-detail/AttemptSummary.tsx +74 -0
  242. package/src/report/components/attempt-detail/AttemptTimeline.tsx +108 -0
  243. package/src/report/components/attempt-detail/AttemptTrace.tsx +63 -0
  244. package/src/report/components/attempt-detail/AttemptUsage.tsx +29 -0
  245. package/src/report/components/attempt-detail/attempt-components.test.tsx +680 -0
  246. package/src/report/components/attempt-detail/compute.ts +287 -0
  247. package/src/report/components/attempt-detail/faces.ts +257 -0
  248. package/src/report/components/attempt-detail/index.tsx +583 -0
  249. package/src/report/components/attempt-detail/validate.test.ts +235 -0
  250. package/src/report/{react → components}/cell.tsx +6 -3
  251. package/src/report/{report.test.ts → components/compute.test.ts} +316 -46
  252. package/src/report/{react → components/entity-lists}/AttemptList.tsx +24 -16
  253. package/src/report/{react → components/entity-lists}/EvalList.tsx +0 -0
  254. package/src/report/{react → components/entity-lists}/ExperimentList.tsx +23 -20
  255. package/src/report/components/entity-lists/compute.ts +196 -0
  256. package/src/report/components/entity-lists/faces.ts +207 -0
  257. package/src/report/components/entity-lists/index.tsx +241 -0
  258. package/src/report/components/entity-lists/validate.test.ts +114 -0
  259. package/src/report/{react → components}/fixtures.ts +2 -2
  260. package/src/report/{react → components/metric-views}/DeltaTable.tsx +5 -5
  261. package/src/report/{react → components/metric-views}/MetricBars.tsx +5 -5
  262. package/src/report/{react → components/metric-views}/MetricLine.tsx +9 -6
  263. package/src/report/{react → components/metric-views}/MetricMatrix.tsx +6 -6
  264. package/src/report/{react → components/metric-views}/MetricScatter.tsx +41 -33
  265. package/src/report/{react → components/metric-views}/MetricTable.tsx +6 -6
  266. package/src/report/{react → components/metric-views}/Scoreboard.tsx +4 -4
  267. package/src/report/{compute.ts → components/metric-views/compute.ts} +51 -477
  268. package/src/report/components/metric-views/faces.ts +423 -0
  269. package/src/report/components/metric-views/index.tsx +352 -0
  270. package/src/report/{text → components/metric-views}/plot.ts +1 -1
  271. package/src/report/components/metric-views/validate.test.ts +211 -0
  272. package/src/report/{react → components}/render.test.tsx +47 -17
  273. package/src/report/components/shared-compute.ts +59 -0
  274. package/src/report/components/shared-faces.ts +30 -0
  275. package/src/report/components/shared.ts +192 -0
  276. package/src/report/{react → components/site-components}/CopyFixPrompt.tsx +3 -3
  277. package/src/report/{react → components/site-components}/HeroCard.tsx +3 -3
  278. package/src/report/{react → components/site-components}/ScopeWarnings.tsx +4 -4
  279. package/src/report/{react → components/site-components}/TraceWaterfall.tsx +13 -10
  280. package/src/report/components/site-components/compute.ts +144 -0
  281. package/src/report/components/site-components/faces.ts +80 -0
  282. package/src/report/components/site-components/index.tsx +265 -0
  283. package/src/report/{scope-warnings.ts → components/site-components/scope-warnings.ts} +3 -3
  284. package/src/report/{site-components.test.tsx → components/site-components/site-components.test.tsx} +23 -21
  285. package/src/report/components/site-components/validate.test.ts +89 -0
  286. package/src/report/{react → components/summaries}/ScopeSummary.tsx +26 -11
  287. package/src/report/components/summaries/compute.ts +56 -0
  288. package/src/report/components/summaries/faces.ts +48 -0
  289. package/src/report/components/summaries/index.tsx +127 -0
  290. package/src/report/components/summaries/validate.test.ts +49 -0
  291. package/src/report/definition/grid-layout.test.ts +124 -0
  292. package/src/report/definition/grid-layout.ts +146 -0
  293. package/src/report/{primitives.tsx → definition/primitives.tsx} +168 -18
  294. package/src/report/{report.ts → definition/report.ts} +98 -165
  295. package/src/report/{shell-head.test.ts → definition/shell-head.test.ts} +2 -2
  296. package/src/report/{text/table.ts → definition/table-text.ts} +4 -4
  297. package/src/report/{tree.ts → definition/tree.ts} +58 -22
  298. package/src/report/index.ts +126 -66
  299. package/src/report/{aggregate.ts → model/aggregate.ts} +31 -9
  300. package/src/report/{flag.ts → model/flag.ts} +39 -1
  301. package/src/report/{format.ts → model/format.ts} +59 -5
  302. package/src/report/{locale.ts → model/locale.ts} +34 -36
  303. package/src/report/{metrics.ts → model/metrics.ts} +3 -3
  304. package/src/report/{types.ts → model/types.ts} +143 -22
  305. package/src/report/react/index.tsx +52 -26
  306. package/src/report/{dual-render.test.tsx → runtime/dual-render.test.tsx} +564 -49
  307. package/src/report/runtime/host.test.ts +44 -0
  308. package/src/report/runtime/host.ts +138 -0
  309. package/src/report/{load.ts → runtime/load.ts} +1 -1
  310. package/src/report/runtime/text.ts +192 -0
  311. package/src/report/runtime/web.ts +106 -0
  312. package/src/results/attempt-evidence.ts +5 -1
  313. package/src/results/host-equivalence.test.ts +54 -3
  314. package/src/results/select.ts +30 -6
  315. package/src/runner/attempt.test.ts +48 -1
  316. package/src/runner/attempt.ts +62 -42
  317. package/src/runner/cleanup-timeout.test.ts +16 -0
  318. package/src/runner/cleanup-timeout.ts +23 -0
  319. package/src/runner/discover.ts +1 -2
  320. package/src/runner/eval-selection.test.ts +187 -0
  321. package/src/runner/eval-selection.ts +109 -0
  322. package/src/runner/experiment-cleanup-registry.test.ts +89 -0
  323. package/src/runner/experiment-cleanup-registry.ts +39 -0
  324. package/src/runner/experiment-labels.test.ts +71 -0
  325. package/src/runner/feedback/agent.test.ts +33 -1
  326. package/src/runner/feedback/agent.ts +12 -16
  327. package/src/runner/feedback/ci.test.ts +35 -1
  328. package/src/runner/feedback/ci.ts +16 -18
  329. package/src/runner/feedback/coordinator.ts +28 -0
  330. package/src/runner/feedback/human.test.ts +108 -1
  331. package/src/runner/feedback/human.ts +71 -21
  332. package/src/runner/feedback/reducer.test.ts +64 -0
  333. package/src/runner/feedback/reducer.ts +26 -0
  334. package/src/runner/feedback/sink.ts +40 -1
  335. package/src/runner/fingerprint.ts +2 -1
  336. package/src/runner/report.test.ts +14 -0
  337. package/src/runner/report.ts +3 -1
  338. package/src/runner/run.test.ts +390 -12
  339. package/src/runner/run.ts +259 -26
  340. package/src/runner/sandbox-selection.test.ts +13 -3
  341. package/src/runner/sandbox-selection.ts +4 -2
  342. package/src/runner/types.ts +158 -23
  343. package/src/sandbox/docker.ts +7 -0
  344. package/src/sandbox/e2b.ts +12 -13
  345. package/src/sandbox/types.ts +23 -21
  346. package/src/scoring/display.test.ts +28 -2
  347. package/src/scoring/display.ts +38 -5
  348. package/src/scoring/types.ts +3 -3
  349. package/src/shared/aggregate.test.ts +52 -0
  350. package/src/shared/aggregate.ts +30 -1
  351. package/src/shared/types.ts +6 -1
  352. package/src/show/command.test.ts +34 -0
  353. package/src/show/command.ts +16 -0
  354. package/src/show/compose.ts +1 -1
  355. package/src/show/index.ts +58 -22
  356. package/src/show/render.ts +16 -240
  357. package/src/show/show.test.ts +128 -35
  358. package/src/view/app/App.test.tsx +8 -7
  359. package/src/view/app/App.tsx +135 -51
  360. package/src/view/app/i18n.ts +7 -147
  361. package/src/view/app/lib/attempt-dialog.test.ts +65 -0
  362. package/src/view/app/lib/attempt-dialog.ts +69 -0
  363. package/src/view/app/main.tsx +0 -1
  364. package/src/view/app/types.ts +3 -86
  365. package/src/view/artifact-serving.test.ts +22 -38
  366. package/src/view/client-dist/app.css +1 -1
  367. package/src/view/client-dist/app.js +15 -22
  368. package/src/view/data.test.ts +48 -36
  369. package/src/view/data.ts +122 -89
  370. package/src/view/index.ts +3 -13
  371. package/src/view/server.ts +36 -6
  372. package/src/view/shared/types.ts +6 -31
  373. package/src/view/site-parity.test.ts +24 -1
  374. package/src/view/site.ts +144 -17
  375. package/src/view/styles.css +18 -806
  376. package/src/view/view-report.test.ts +182 -34
  377. package/dist/report/components.d.ts +0 -179
  378. package/dist/report/components.js +0 -544
  379. package/dist/report/compute.d.ts +0 -139
  380. package/dist/report/react/ExperimentComparison.d.ts +0 -10
  381. package/dist/report/react/ExperimentComparison.js +0 -12
  382. package/dist/report/react/MetricScatter.d.ts +0 -9
  383. package/dist/report/react/format.d.ts +0 -3
  384. package/dist/report/react/format.js +0 -7
  385. package/dist/report/text/faces.d.ts +0 -45
  386. package/dist/report/text/faces.js +0 -671
  387. package/dist/report/web.d.ts +0 -32
  388. package/dist/report/web.js +0 -48
  389. package/docs-site/zh/how-to/authoring.mdx +0 -162
  390. package/docs-site/zh/how-to/custom-reports.mdx +0 -414
  391. package/docs-site/zh/how-to/experiments.mdx +0 -86
  392. package/docs-site/zh/how-to/write-experiment.mdx +0 -164
  393. package/src/report/components.tsx +0 -925
  394. package/src/report/react/ExperimentComparison.tsx +0 -73
  395. package/src/report/react/format.ts +0 -9
  396. package/src/report/text/faces.ts +0 -767
  397. package/src/report/web.ts +0 -77
  398. package/src/show/report-host.test.ts +0 -205
  399. package/src/show/report-host.ts +0 -389
  400. package/src/view/app/components/AttemptModal.tsx +0 -496
  401. package/src/view/app/components/CodeView.test.tsx +0 -142
  402. package/src/view/app/components/CodeView.tsx +0 -310
  403. package/src/view/app/components/CopyControls.tsx +0 -106
  404. package/src/view/app/components/Trace.tsx +0 -100
  405. package/src/view/app/components/Transcript.tsx +0 -157
  406. package/src/view/app/components/ui/badge.tsx +0 -21
  407. package/src/view/app/lib/artifact-url.ts +0 -17
  408. package/src/view/app/lib/attempt-route.test.ts +0 -80
  409. package/src/view/app/lib/attempt-route.ts +0 -52
  410. package/src/view/app/lib/format.ts +0 -70
  411. package/src/view/app/lib/guards.test.ts +0 -108
  412. package/src/view/app/lib/guards.ts +0 -71
  413. package/src/view/app/lib/rows.ts +0 -22
  414. package/src/view/app/lib/transcript-data.tsx +0 -151
  415. package/src/view/app/lib/verdict.ts +0 -23
  416. package/src/view/app/shared.ts +0 -8
  417. /package/dist/report/{react → components/metric-views}/chart-math.d.ts +0 -0
  418. /package/dist/report/{react → components/metric-views}/chart-math.js +0 -0
  419. /package/dist/report/{text → components/metric-views}/plot.d.ts +0 -0
  420. /package/dist/report/{react → components/site-components}/PoweredBy.d.ts +0 -0
  421. /package/dist/report/{react → components/site-components}/PoweredBy.js +0 -0
  422. /package/dist/report/{text/layout.d.ts → model/text-layout.d.ts} +0 -0
  423. /package/dist/report/{text/layout.js → model/text-layout.js} +0 -0
  424. /package/dist/report/{types.js → model/types.js} +0 -0
  425. /package/src/report/{react → components/metric-views}/chart-math.test.ts +0 -0
  426. /package/src/report/{react → components/metric-views}/chart-math.ts +0 -0
  427. /package/src/report/{react → components/site-components}/PoweredBy.tsx +0 -0
  428. /package/src/report/{text/layout.ts → model/text-layout.ts} +0 -0
@@ -14,10 +14,19 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { afterEach, describe, expect, it } from "vitest";
17
- import { IncompatibleResultsError, ViewInputError, loadLatestResultsPerEval, loadViewScan } from "./data.ts";
17
+ import { IncompatibleResultsError, ViewInputError, loadLatestResultsPerEval, loadViewScan, type ViewScan } from "./data.ts";
18
+ import type { AttemptHandle } from "../results/index.ts";
18
19
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
19
20
  import { encodeAttemptLocator } from "../results/locator.ts";
20
21
 
22
+ /** scan.attemptsByBase 按 base 建索引;测试按 eval id 找回单个 attempt(该 eval 在本 fixture 里唯一时用)。 */
23
+ function attemptByEvalId(scan: ViewScan, evalId: string): AttemptHandle {
24
+ for (const handle of scan.attemptsByBase.values()) {
25
+ if (handle.evalId === evalId) return handle;
26
+ }
27
+ throw new Error(`no attempt found for eval id "${evalId}"`);
28
+ }
29
+
21
30
  const roots: string[] = [];
22
31
  async function makeRoot(): Promise<string> {
23
32
  const root = await mkdtemp(join(tmpdir(), "niceeval-viewdata-"));
@@ -132,9 +141,9 @@ describe("loadViewScan · skipped 三种原因进 viewData", () => {
132
141
  const incomplete = [...byReason.values()].find((s) => s.reason === "incomplete")!;
133
142
  expect(incomplete.dir).toContain("2026-07-05T08-00-00-000Z");
134
143
 
135
- // 正常快照照常进报告槽(默认报告的榜单),坏快照不拖垮整页。
144
+ // 正常快照照常进报告槽(默认报告的榜单),坏快照不拖垮整页;证据室索引也只有 exp/a 一条。
136
145
  expect(reportHtml.en).toContain("exp/a");
137
- expect(viewData.snapshots.map((s) => s.experimentId)).toEqual(["exp/a"]);
146
+ expect([...scan.attemptsByBase.values()].map((a) => a.experimentId)).toEqual(["exp/a"]);
138
147
  });
139
148
 
140
149
  it("单文件模式指向版本不同的报告:抛 IncompatibleResultsError(CLI 打印提示退出)", async () => {
@@ -182,24 +191,29 @@ describe("loadViewScan · 报告槽是现刻水位口径,裸跑与局部收窄
182
191
  evalId: "q2",
183
192
  attempt: 0,
184
193
  });
185
- expect(reportHtml.en).toContain(`#/attempt/${q1Locator}`); // q1 来自周二
186
- expect(reportHtml.en).toContain(`#/attempt/${q2Locator}`); // q2 来自周一
194
+ expect(reportHtml.en).toContain(`attempt/${encodeURIComponent(q1Locator)}.html`); // q1 来自周二
195
+ expect(reportHtml.en).toContain(`attempt/${encodeURIComponent(q2Locator)}.html`); // q2 来自周一
187
196
 
188
197
  // 两题都有真实判定,不是伪残缺:报告槽不再出 partial-coverage 警告。
189
198
  expect(reportHtml.en).not.toContain('data-kind="partial-coverage"');
190
199
 
191
- // 证据室的 latest 标记仍按 results.latest() 口径(周二快照),与报告槽 Selection 无关。
192
- const latest = viewData.snapshots.filter((s) => s.latest);
193
- expect(latest).toHaveLength(1);
194
- expect(latest[0]!.startedAt).toBe("2026-07-02T08:00:00.000Z");
195
200
  expect(viewData.lastRunAt).toBe("2026-07-02T08:00:00.000Z");
196
201
  // 合成 Selection 的 attempts 来自周一、周二两个物理 run。
197
202
  expect(viewData.composedRuns).toBe(2);
198
203
 
199
- // 历史快照仍在(Runs / Traces 吃全部),但不是 latest。
200
- const historical = viewData.snapshots.filter((s) => !s.latest);
201
- expect(historical).toHaveLength(1);
202
- expect(historical[0]!.results.map((r) => r.id).sort()).toEqual(["q1", "q2"]);
204
+ // 历史 attempt 仍各自可达(attempt/<locator>.html 的证据室索引,不随报告槽 Selection 收窄):
205
+ // 周二的 q1(现刻水位选中的那份)、周一的 q2(唯一一次,现刻水位也选它)都在;
206
+ // 周一自己的 q1(被周二顶替,不再是现刻水位选中的那份)locator 不同,dedup 也不吞它——
207
+ // 它是独立的历史事实,不是 --resume 字面携带的复印件。
208
+ expect(scan.attemptPages!.locators.has(q1Locator)).toBe(true);
209
+ expect(scan.attemptPages!.locators.has(q2Locator)).toBe(true);
210
+ const q1MondayLocator = encodeAttemptLocator({
211
+ experimentId: "exp/a",
212
+ snapshotStartedAt: "2026-07-01T08:00:00.000Z",
213
+ evalId: "q1",
214
+ attempt: 0,
215
+ });
216
+ expect(scan.attemptPages!.locators.has(q1MondayLocator)).toBe(true);
203
217
  });
204
218
  });
205
219
 
@@ -224,20 +238,19 @@ describe("loadViewScan · 跨快照去重(--resume 携带的复印件只算一
224
238
  res("q2", "passed"),
225
239
  ]);
226
240
 
227
- const { viewData, artifactDirs } = await loadViewScan(root);
228
- // 全部快照(Runs/Traces 的数据面)里 q1 只出现一次:复印件不灌票。
229
- const allResults = viewData.snapshots.flatMap((s) => s.results);
230
- expect(allResults.filter((r) => r.id === "q1")).toHaveLength(1);
231
- const q1 = allResults.find((r) => r.id === "q1")!;
241
+ const scan = await loadViewScan(root);
242
+ const { artifactDirs, attemptsByBase } = scan;
243
+ // 全部落盘(证据室索引的数据面) q1 只出现一次:复印件不灌票。
244
+ const allAttempts = [...attemptsByBase.values()];
245
+ expect(allAttempts.filter((a) => a.evalId === "q1")).toHaveLength(1);
246
+ const q1 = allAttempts.find((a) => a.evalId === "q1")!;
232
247
  // 证据身份(locator)跟着最新落盘走:身份元组里的 snapshotStartedAt 是新快照的 startedAt。
233
248
  expect(q1.locator).toBe(
234
249
  encodeAttemptLocator({ experimentId: "exp/a", snapshotStartedAt: "2026-07-02T08:00:00.000Z", evalId: "q1", attempt: 0 }),
235
250
  );
236
- expect(q1.artifactBase).toBe("exp_a/2026-07-01T08-00-00-000Z/q1/a0"); // artifact 仍指原快照
251
+ expect(q1.result.artifactBase).toBe("exp_a/2026-07-01T08-00-00-000Z/q1/a0"); // artifact 仍指原快照
237
252
  // 静态导出(--out)能把携带条目的 artifact 一并带走。
238
253
  expect(artifactDirs.get("exp_a/2026-07-01T08-00-00-000Z/q1/a0")).toBe(join(oldDir, "q1", "a0"));
239
- // 条目全被吸走的旧快照不再出现。
240
- expect(viewData.snapshots.filter((s) => !s.latest)).toHaveLength(0);
241
254
  });
242
255
  });
243
256
 
@@ -256,28 +269,27 @@ describe("loadViewScan · 新布局落盘直接可读(写入面 / 读取面同
256
269
  );
257
270
 
258
271
  const scan = await loadViewScan(root);
259
- const { viewData } = scan;
260
272
  const reportHtml = scan.reportPages[0]!.html;
261
273
  // 报告槽(ExperimentComparison 的 web 面):Experiment 工作台行 + 官方通过率格子(1 过 1 败 = 50%)。
262
274
  expect(reportHtml.en).toContain("compare/bub");
263
275
  expect(reportHtml.en).toContain("50%");
264
- const snapshot = viewData.snapshots[0]!;
265
- expect(snapshot.latest).toBe(true);
266
- expect(snapshot.agent).toBe("bub");
267
- expect(snapshot.run).toBe("compare_bub/2026-07-03T08-00-00-000Z");
276
+ const attempts = [...scan.attemptsByBase.values()];
277
+ expect(attempts.every((a) => a.snapshot.agent === "bub")).toBe(true);
278
+ expect(attempts.every((a) => a.snapshot.startedAt === "2026-07-03T08:00:00.000Z")).toBe(true);
268
279
  // 每条结果的 locator 都能由身份元组(experimentId/快照 startedAt/evalId/attempt 下标)独立复算,
269
280
  // 证明它不是随手塞的占位值,而是真从落盘产物(snapshot.json + result.json)算出来的。
270
- expect(snapshot.results.every((r) => r.locator === encodeAttemptLocator({
271
- experimentId: snapshot.experimentId,
272
- snapshotStartedAt: snapshot.startedAt,
273
- evalId: r.id,
274
- attempt: r.attempt,
281
+ expect(attempts.every((a) => a.locator === encodeAttemptLocator({
282
+ experimentId: a.experimentId,
283
+ snapshotStartedAt: a.snapshot.startedAt,
284
+ evalId: a.evalId,
285
+ attempt: a.result.attempt,
275
286
  }))).toBe(true);
276
- // 本快照跑出的条目落盘没有 artifactBase 字段:读取面按 `${ref.snapshot}/${ref.attempt}` 现算,
277
- // ref.snapshot 恒等于 snapshot.run,ref.attempt 恒是 `${evalId}/a${attempt}`。
278
- const q1 = snapshot.results.find((r) => r.id === "q1")!;
279
- expect(q1.hasEvents).toBe(true);
280
- expect(q1.artifactBase).toBe(`${snapshot.run}/q1/a0`);
287
+ // 本快照跑出的条目落盘没有 artifactBase 字段:读取面按 `${ref.snapshot}/${ref.attempt}` 现算——
288
+ // scan.attemptsByBase 正是按这份现算出的 base 建的索引,q1 应恰好落在这个 key 上。
289
+ const q1Base = "compare_bub/2026-07-03T08-00-00-000Z/q1/a0";
290
+ const q1 = scan.attemptsByBase.get(q1Base)!;
291
+ expect(q1).toBeTruthy();
292
+ expect(q1.result.hasEvents).toBe(true);
281
293
  });
282
294
  });
283
295
 
package/src/view/data.ts CHANGED
@@ -7,28 +7,23 @@
7
7
  // 见 docs/feature/reports/view.md「打开与收窄」。
8
8
 
9
9
  import { readFileSync, statSync } from "node:fs";
10
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
11
- import { dedupeAttempts, openResults } from "../results/index.ts";
10
+ import { dirname, extname, join, relative, resolve } from "node:path";
11
+ import { dedupeAttempts, loadAttemptEvidence, openResults } from "../results/index.ts";
12
12
  import type { AttemptHandle, Results, Scope, Snapshot, SkippedDir } from "../results/index.ts";
13
+ import type { AttemptLocator } from "../results/locator.ts";
13
14
  import {
15
+ buildHostReportMeta,
14
16
  loadHostReport,
15
17
  renderHostPageHtml,
16
- reportMetaFor,
17
- resolveReportTitle,
18
- type HostHeadTag,
19
- type HostReport,
20
- type HostReportAsset,
21
- } from "../show/report-host.ts";
18
+ type ReportAsset,
19
+ type ReportDefinition,
20
+ type ReportPage,
21
+ type HeadTag,
22
+ } from "../report/runtime/host.ts";
22
23
  import { selectCurrentResults, filterExperiments } from "../results/select.ts";
24
+ import { evalPrefixPredicate } from "../shared/aggregate.ts";
23
25
  import type { EvalResult } from "../types.ts";
24
- import type {
25
- SkippedRunNotice,
26
- ViewData,
27
- ViewEvalResult,
28
- ViewReportMeta,
29
- ViewReportPageHtml,
30
- ViewSnapshot,
31
- } from "./shared/types.ts";
26
+ import type { SkippedRunNotice, ViewData, ViewReportMeta, ViewReportPageHtml } from "./shared/types.ts";
32
27
  import { t } from "../i18n/index.ts";
33
28
  import { RESULTS_SCHEMA_VERSION } from "../types.ts";
34
29
 
@@ -57,13 +52,31 @@ export interface ViewScan {
57
52
  reportPages: ViewReportPageHtml[];
58
53
  /** 外壳注入资产(styles / scripts;{src} 已按路径纪律解析成 inline 内容),只进 web 面。 */
59
54
  shellAssets: { styles: string[]; scripts: string[]; head: ResolvedHeadTag[] };
55
+ /**
56
+ * 报告声明了 attempt-input page 时才存在(architecture.md「Attempt 详情是一张参数化 page」)。
57
+ * `locators` 是收窄后有效根内可达的 locator → AttemptHandle(与 scope-input pages 同一份
58
+ * `scopedExperiments ∩ matchEval ∩ survivors` 口径——去重只吞掉 `--resume` 携带的字面重复,
59
+ * 不排除真实历史 attempt,见 view.md「打开与收窄」);站点管线(site.ts)据此为每个可达
60
+ * locator 生成一份 `attempt/<locator>.html`,不在这份索引里的 locator 不出站。
61
+ * `render` 装配一个 locator 的 `AttemptEvidence` 并渲染该 page 两种语言的内容 HTML(不含外层
62
+ * 文档 —— 独立 HTML 文档的组装是 site.ts 的事);pageFailure 语义与 scope pages 一致。
63
+ */
64
+ attemptPages?: {
65
+ page: ReportPage;
66
+ locators: Map<AttemptLocator, AttemptHandle>;
67
+ render(locator: AttemptLocator, handle: AttemptHandle): Promise<{ en: string; "zh-CN": string }>;
68
+ };
60
69
  }
61
70
 
71
+ /** attempt 页面内容(不是 index.html 的 scope-input page)链去同目录 attempt/ 下的兄弟文档,
72
+ * 不带 `attempt/` 目录前缀(site.ts「站点管线」;两处 href 的对应关系与拆分只住在那)。 */
73
+ const SIBLING_ATTEMPT_HREF = (locator: AttemptLocator): string => `${encodeURIComponent(locator)}.html`;
74
+
62
75
  /** view 宿主输入的组合语义(与 show 对齐,docs/feature/reports/architecture.md「Scope 是计算入口」)。 */
63
76
  export interface ViewScanOptions {
64
- /** eval id 前缀(位置参数):收窄报告槽 Scope;证据室(快照明细)不收窄,深链恒可达。 */
77
+ /** eval id 前缀(位置参数):把根滤成有效根,页面 Scope 与证据(快照明细、artifact 清单)一致收窄。 */
65
78
  patterns?: string[];
66
- /** experiment id 前缀(--experiment):Scope 只留该实验。 */
79
+ /** experiment id 前缀(--exp):有效根只留匹配实验。 */
67
80
  experiment?: string;
68
81
  /** --report 报告文件:相对 cwd 的路径。装载失败抛 ReportLoadError(CLI 打印后退出)。 */
69
82
  report?: { path: string; cwd: string };
@@ -175,10 +188,12 @@ export async function loadLatestResultsPerEval(root = ".niceeval"): Promise<Eval
175
188
  /**
176
189
  * `niceeval view` 的数据装载入口:server 每次请求现读现算,`--out` 导出用同一份。
177
190
  * 报告槽 Selection 恒经 selectCurrentResults 合成(现刻水位;与 `niceeval show` 调同一个
178
- * 函数,裸跑与局部收窄不分叉),位置前缀 / --experiment 只作为 scope 传入,不切换选择口径。
191
+ * 函数,裸跑与局部收窄不分叉),位置前缀 / --exp 只作为 scope 传入,不切换选择口径。
179
192
  * --report 本身不改挑选——它只换报告槽的填充,注入的 Selection 与裸跑同一份,
180
193
  * 「裸跑 ≡ --report <ExperimentComparison>」靠这条成立(docs/feature/reports/architecture.md「Selection 是计算入口」)。
181
- * 证据室数据(快照明细 / skipped)恒为全量,深链在任何收窄下都可达。
194
+ * 命令行收窄作用在有效根上(docs/feature/reports/view.md 开篇):证据室数据与 artifact 清单
195
+ * 与页面一致地只含收窄后的范围,本地与导出无分叉——收窄导出的站点(烘进 HTML 的数据、
196
+ * 证据文件)只含收窄到的内容。收窄之内、不在现刻水位里的历史 attempt 仍在有效根里,深链可达。
182
197
  * 零可读结果一律抛 ViewInputError,不渲染/导出空页面(server 起不来,--out 非零退出)。
183
198
  */
184
199
  export async function loadViewScan(input?: string, opts: ViewScanOptions = {}): Promise<ViewScan> {
@@ -210,9 +225,6 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
210
225
 
211
226
  // 报告槽 Selection:恒经现刻水位选择器合成,与 show 裸跑同口径(两扇门判定不分叉)。
212
227
  const selection = selectCurrentResults(results, { experiment: opts.experiment, patterns });
213
- // latestPerExperiment 只服务证据室 UI 的 latest 标记(ViewSnapshot.latest / viewData.snapshots),
214
- // 与报告槽 Selection 完全无关,绝不复用为报告 Selection。
215
- const latestPerExperiment = results.latest();
216
228
 
217
229
  if (patterns.length > 0 && selection.snapshots.every((s) => s.evals.length === 0)) {
218
230
  const known = [
@@ -228,49 +240,34 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
228
240
  // 静态渲染成 HTML(en / zh-CN 各一遍,切界面语言不重算数据)。
229
241
  const slot = await renderReportSlot(opts.report, opts.page, results, selection, opts.pageFailure ?? "throw");
230
242
 
243
+ // 有效根:命令行收窄把根滤成只含匹配实验与 attempt(docs/feature/reports/view.md 开篇)。
244
+ // 证据室数据与 artifact 清单从这里取数,与页面 Scope 一致收窄——本地与导出无分叉,
245
+ // 收窄导出的站点(烘进 HTML 的数据、证据文件)只含收窄后的范围。
246
+ const scopedExperiments = filterExperiments(results.experiments, opts.experiment);
247
+ const matchEval = patterns.length > 0 ? evalPrefixPredicate(patterns) : () => true;
248
+
231
249
  // 跨快照按身份键去重:--resume 携带的条目在多份落盘里重复,只保留最新快照里的那份
232
- // (与官方计算函数的聚合口径一致,Runs / Traces 的计数因此不被复印件灌票)。
250
+ // (与官方计算函数的聚合口径一致,attempt/<locator>.html 的计数因此不被复印件灌票)。
233
251
  const artifactDirs = new Map<string, string>();
234
252
  const attemptsByBase = new Map<string, AttemptHandle>();
235
- // latest 标记恒按 results.latest() 口径打(ViewSnapshot.latest 的声明语义),
236
- // 与报告槽 Selection(现刻水位,可能合成自更早快照)是两个独立概念,不混用。
237
- const latestSet = new Set(latestPerExperiment.snapshots);
253
+ // 报告没有 attempt-input page 时不建这份索引:没有 attempt/ 目录,站点管线不需要它
254
+ // (view.md「静态导出」)
255
+ const attemptsByLocator = slot.attemptPage ? new Map<AttemptLocator, AttemptHandle>() : undefined;
238
256
  const allAttempts: AttemptHandle[] = [];
239
- for (const exp of results.experiments) {
240
- for (const snap of exp.snapshots) allAttempts.push(...snap.attempts);
257
+ for (const exp of scopedExperiments) {
258
+ for (const snap of exp.snapshots) allAttempts.push(...snap.attempts.filter((a) => matchEval(a.evalId)));
241
259
  }
242
- const survivors = new Set(dedupeAttempts(allAttempts).attempts);
243
-
244
- const snapshots: ViewSnapshot[] = [];
245
- for (const exp of results.experiments) {
246
- for (const snap of exp.snapshots) {
247
- const kept = snap.attempts.filter((a) => survivors.has(a));
248
- const latest = latestSet.has(snap);
249
- // 条目全被去重吸走的历史快照不再携带(它的内容原样活在更新的落盘里)。
250
- if (kept.length === 0 && !latest) continue;
251
- snapshots.push({
252
- experimentId: snap.experimentId,
253
- agent: snap.agent,
254
- ...(snap.model !== undefined ? { model: snap.model } : {}),
255
- startedAt: snap.startedAt,
256
- // 与 reader 的 AttemptRef.snapshot 同一公式(祖父目录名/自身目录名),不依赖 root——
257
- // 单文件模式的 root 是从目标文件上跳算出的,两条计算各自独立更不容易踩偏差。
258
- run: `${basename(dirname(snap.dir))}/${basename(snap.dir)}`,
259
- latest,
260
- results: kept.map((a) => {
261
- const { annotated, base, abs } = annotateResult(a, root);
262
- artifactDirs.set(base, abs);
263
- attemptsByBase.set(base, a);
264
- return annotated;
265
- }),
266
- });
267
- }
260
+ for (const attempt of dedupeAttempts(allAttempts).attempts) {
261
+ const { base, abs } = artifactLocation(attempt, root);
262
+ artifactDirs.set(base, abs);
263
+ attemptsByBase.set(base, attempt);
264
+ if (attemptsByLocator && attempt.locator !== undefined) attemptsByLocator.set(attempt.locator, attempt);
268
265
  }
269
266
 
270
- // 全局最新快照(跨全部实验):viewData.lastRunAt 从这里取。页内 hero 的「最后运行」
267
+ // 全局最新快照(跨有效根内全部实验):viewData.lastRunAt 从这里取。页内 hero 的「最后运行」
271
268
  // 显示由 Hero 组件按 heroData(scope) 自己算,不吃这份字段。
272
269
  let latestSnapshot: Snapshot | undefined;
273
- for (const exp of results.experiments) {
270
+ for (const exp of scopedExperiments) {
274
271
  const candidate = exp.snapshots[0];
275
272
  if (!candidate) continue;
276
273
  if (!latestSnapshot || candidate.startedAt > latestSnapshot.startedAt) latestSnapshot = candidate;
@@ -282,7 +279,6 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
282
279
  // 反向引用取——每个 attempt 的 .snapshot 恒指向它真实所在的贡献快照(无论 Scope
283
280
  // 是否合成),所以这条对裸跑与收窄一律成立,不需要分支。
284
281
  composedRuns: new Set(selection.snapshots.flatMap((s) => s.attempts.map((a) => a.snapshot.dir))).size,
285
- snapshots,
286
282
  skippedRuns: results.skipped.map(toSkippedNotice),
287
283
  report: slot.meta,
288
284
  };
@@ -292,6 +288,9 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
292
288
  attemptsByBase,
293
289
  reportPages: slot.pages,
294
290
  shellAssets: slot.shellAssets,
291
+ ...(slot.attemptPage && attemptsByLocator
292
+ ? { attemptPages: { page: slot.attemptPage, locators: attemptsByLocator, render: slot.renderAttemptPage } }
293
+ : {}),
295
294
  };
296
295
  }
297
296
 
@@ -334,7 +333,7 @@ export interface ResolvedHeadTag {
334
333
  }
335
334
 
336
335
  /** head 标签 attrs 的 src/href 解析:外链透传,本地路径按 `{src}` 同一路径纪律落成绝对路径并验存在。 */
337
- function resolveShellHead(tags: HostHeadTag[], baseDir: string | undefined): ResolvedHeadTag[] {
336
+ function resolveShellHead(tags: readonly HeadTag[], baseDir: string | undefined): ResolvedHeadTag[] {
338
337
  const out: ResolvedHeadTag[] = [];
339
338
  for (const entry of tags) {
340
339
  const attrs = { ...(entry.attrs ?? {}) };
@@ -367,7 +366,7 @@ function resolveShellHead(tags: HostHeadTag[], baseDir: string | undefined): Res
367
366
  }
368
367
 
369
368
  /** 外壳 `{src}` 资产的路径纪律(shell.md「行为约束」):相对报告文件解析;拒绝 `..` 路径段、绝对路径与 `~`。 */
370
- function resolveShellAssets(assets: HostReportAsset[], baseDir: string | undefined, kind: "styles" | "scripts"): string[] {
369
+ function resolveShellAssets(assets: readonly ReportAsset[], baseDir: string | undefined, kind: "styles" | "scripts"): string[] {
371
370
  const out: string[] = [];
372
371
  for (const asset of assets) {
373
372
  if (asset.inline !== undefined) {
@@ -410,17 +409,32 @@ async function renderReportSlot(
410
409
  meta: ViewReportMeta;
411
410
  pages: ViewReportPageHtml[];
412
411
  shellAssets: { styles: string[]; scripts: string[]; head: ResolvedHeadTag[] };
412
+ attemptPage: ReportPage | undefined;
413
+ renderAttemptPage: (locator: AttemptLocator, handle: AttemptHandle) => Promise<{ en: string; "zh-CN": string }>;
413
414
  }> {
414
415
  // 报告 runtime 走预编译产物(dist/report/**,`pnpm run build:report` 产出),不受 view
415
- // 消费方 cwd/tsconfig 影响;装载与渲染统一经 ../show/report-host.ts(两个宿主共用的联系面)。
416
- const hostReport: HostReport = await loadHostReport(report?.cwd ?? process.cwd(), report?.path, {
416
+ // 消费方 cwd/tsconfig 影响;装载与渲染统一经 ../report/runtime/host.ts(两个宿主共用的中性联系面)。
417
+ const hostReport: ReportDefinition = await loadHostReport(report?.cwd ?? process.cwd(), report?.path, {
417
418
  freshImport: true,
418
419
  });
419
420
 
420
- const initialPageId = page ?? hostReport.pages[0]!.id;
421
- if (!hostReport.pages.some((p) => p.id === initialPageId)) {
421
+ // scope-input pages 只有这些参与本函数的「全部烘进 index.html」渲染;attempt-input page(如果
422
+ // 报告声明了)没有 locator 就不能 resolve,它的每-locator 静态文档是独立机制,不在这里渲染
423
+ // (docs/feature/reports/architecture.md「Attempt 详情是一张参数化 page」)。
424
+ const scopePages = hostReport.pages.filter((p) => p.input !== "attempt");
425
+ const navigablePages = scopePages.filter((p) => p.navigation !== false);
426
+
427
+ const initialPageId = page ?? navigablePages[0]?.id ?? scopePages[0]?.id;
428
+ const initialPage = initialPageId !== undefined ? scopePages.find((p) => p.id === initialPageId) : undefined;
429
+ if (initialPageId === undefined || !initialPage) {
430
+ const requested = hostReport.pages.find((p) => p.id === page);
431
+ if (requested?.input === "attempt") {
432
+ throw new ViewInputError(
433
+ `error: page "${page}" in ${report?.path ?? "the built-in report"} is an attempt-input page and needs a locator — it cannot be opened as the initial page directly.`,
434
+ );
435
+ }
422
436
  throw new ViewInputError(
423
- `error: page "${page}" not found in ${report?.path ?? "the built-in report"}. Available pages: ${hostReport.pages.map((p) => p.id).join(", ")}`,
437
+ `error: page "${page}" not found in ${report?.path ?? "the built-in report"}. Available pages: ${navigablePages.map((p) => p.id).join(", ")}`,
424
438
  );
425
439
  }
426
440
 
@@ -431,10 +445,10 @@ async function renderReportSlot(
431
445
  head: resolveShellHead(hostReport.head, reportDir),
432
446
  };
433
447
 
448
+ const hostMeta = await buildHostReportMeta(hostReport, selection);
434
449
  const pages: ViewReportPageHtml[] = [];
435
- for (const hostPage of hostReport.pages) {
436
- const meta = reportMetaFor(hostReport, hostPage.id, selection.snapshots);
437
- const ctx = { scope: selection, results, report: meta };
450
+ for (const hostPage of scopePages) {
451
+ const ctx = { scope: selection, results, report: hostMeta, page: { id: hostPage.id, input: "scope" as const } };
438
452
  try {
439
453
  pages.push({
440
454
  id: hostPage.id,
@@ -453,13 +467,42 @@ async function renderReportSlot(
453
467
  }
454
468
 
455
469
  const meta: ViewReportMeta = {
456
- title: resolveReportTitle(hostReport.title, selection.snapshots),
457
- links: hostReport.links,
470
+ title: hostMeta.title,
471
+ links: [...hostReport.links],
458
472
  ...(hostReport.footer !== undefined ? { footer: hostReport.footer } : {}),
459
- pages: hostReport.pages.map((p) => ({ id: p.id, title: p.title })),
473
+ pages: scopePages.map((p) => ({ id: p.id, title: p.title })),
460
474
  initialPageId,
461
475
  };
462
- return { meta, pages, shellAssets };
476
+
477
+ const attemptPage = hostReport.pages.find((p) => p.input === "attempt");
478
+ // 装配一个 locator 的 AttemptEvidence 并渲染该 page 两种语言的内容 HTML(不含外层文档);
479
+ // pageFailure 语义与 scope pages 相同 —— 本地 server 折成该文档的完整错误反馈块,
480
+ // 静态导出直接抛出(writeSite 侧汇总成「整体失败,不留半套目录」)。
481
+ const renderAttemptPage = async (
482
+ locator: AttemptLocator,
483
+ handle: AttemptHandle,
484
+ ): Promise<{ en: string; "zh-CN": string }> => {
485
+ const evidence = await loadAttemptEvidence(handle);
486
+ const ctx = {
487
+ scope: selection,
488
+ results,
489
+ report: hostMeta,
490
+ page: { id: attemptPage!.id, input: "attempt" as const, locator, evidence },
491
+ };
492
+ try {
493
+ return {
494
+ en: await renderHostPageHtml(attemptPage!, ctx, { locale: "en", attemptHref: SIBLING_ATTEMPT_HREF }),
495
+ "zh-CN": await renderHostPageHtml(attemptPage!, ctx, { locale: "zh-CN", attemptHref: SIBLING_ATTEMPT_HREF }),
496
+ };
497
+ } catch (e) {
498
+ if (pageFailure !== "embed") throw e;
499
+ const message = e instanceof Error ? e.message : String(e);
500
+ const block = `<div class="nre nre-page-error"><pre>${escapeHtml(message)}</pre></div>`;
501
+ return { en: block, "zh-CN": block };
502
+ }
503
+ };
504
+
505
+ return { meta, pages, shellAssets, attemptPage, renderAttemptPage };
463
506
  }
464
507
 
465
508
  /**
@@ -493,24 +536,14 @@ function assertSingleFileReadable(results: Results, target: string): void {
493
536
  }
494
537
 
495
538
  /**
496
- * 给单条 attempt 注入 view 侧标注:
497
- * - locator:不透明的 AttemptLocator(与 Reports 的 MetricCell.refs / `ctx.attemptHref` 同一身份),
498
- * `#/attempt/@<locator>` 深链路由的参数——证据室按它在 viewData.snapshots 里定位回同一条 attempt。
499
- * - artifactBase:相对 view 根的 artifact 目录(前端据此 fetch trace.json 等)。本快照跑出的
500
- * 条目落盘没有这个字段,按 `${ref.snapshot}/${ref.attempt}` 现算;携带条目(--resume 合入)
501
- * 落盘自带 artifactBase,指向原快照,原样沿用。
502
- * 返回新对象,不 mutate 读入的结果;宿主机绝对路径只回给调用方写进 artifactDirs
503
- * (server 端内存),不挂到 result 上,避免随 viewData 进静态 HTML。
539
+ * 一条 attempt artifact 目录:相对 view 根的 base(前端据此 fetch trace.json 等)与宿主机
540
+ * 绝对路径。本快照跑出的条目落盘没有 artifactBase 字段,按 `${ref.snapshot}/${ref.attempt}`
541
+ * 现算;携带条目(--resume 合入)落盘自带 artifactBase,指向原快照,原样沿用。绝对路径只
542
+ * 回给调用方写进 artifactDirs(server 端内存),不进 viewData,避免随静态 HTML 泄漏宿主机路径。
504
543
  */
505
- function annotateResult(
506
- attempt: AttemptHandle,
507
- root: string,
508
- ): { annotated: ViewEvalResult; base: string; abs: string } {
509
- const r = attempt.result;
510
- const base = r.artifactBase ?? `${attempt.ref.snapshot}/${attempt.ref.attempt}`;
511
- const abs = join(root, base);
512
- const annotated: ViewEvalResult = { ...r, locator: attempt.locator, artifactBase: base };
513
- return { annotated, base, abs };
544
+ function artifactLocation(attempt: AttemptHandle, root: string): { base: string; abs: string } {
545
+ const base = attempt.result.artifactBase ?? `${attempt.ref.snapshot}/${attempt.ref.attempt}`;
546
+ return { base, abs: join(root, base) };
514
547
  }
515
548
 
516
549
  /**
package/src/view/index.ts CHANGED
@@ -10,7 +10,7 @@ import { planSite, writeSite } from "./site.ts";
10
10
  import type { ViewOptions } from "./server.ts";
11
11
 
12
12
  export { startViewServer, type ViewOptions, type ViewServer } from "./server.ts";
13
- export { planSite, writeSite, renderHtml, type SitePlan, type SiteFile } from "./site.ts";
13
+ export { planSite, writeSite, renderHtml, renderStandaloneAttemptDocument, type SitePlan, type SiteFile } from "./site.ts";
14
14
  export {
15
15
  IncompatibleResultsError,
16
16
  ViewInputError,
@@ -80,18 +80,8 @@ export async function buildView(opts: ViewOptions = {}): Promise<string> {
80
80
  `--out expects a directory, got "${opts.out}". Single-file HTML export was removed: code, transcript and trace views need artifact files next to the page. Export a directory instead (e.g. --out site) and serve it with any static host.`,
81
81
  );
82
82
  }
83
- // --out 与位置参数 / --experiment 互斥(docs/feature/reports/view.md「静态导出」):报告槽收窄
84
- // 只影响报告,证据室恒随根完整——允许同用会让发布者误以为站点只含该实验,实际根里全部
85
- // attempt 的证据都已出站。按实验收窄发布 = 用 copySnapshots 构建只含它的发布根,再对新根导出。
86
- if ((opts.scan?.patterns?.length ?? 0) > 0 || opts.scan?.experiment !== undefined) {
87
- throw new ViewInputError(
88
- "--out exports the whole results root and cannot be combined with eval prefixes or --experiment.\n" +
89
- "To publish a site for one experiment, build a narrower results root and export that:\n" +
90
- ' const results = await openResults(".niceeval");\n' +
91
- ' await copySnapshots(results.latest().filter((s) => s.experimentId.startsWith("<prefix>/")), "<publish-root>");\n' +
92
- "Then: niceeval view --results <publish-root> --out <site>",
93
- );
94
- }
83
+ // 位置参数 / --exp 对导出同义于本地:收窄作用在有效根上,出站的页面数据与证据文件
84
+ // 只含收窄后的范围(docs/feature/reports/view.md「静态导出」:出站的就是收窄到的)。
95
85
  // 静态导出保持「任一页失败整体失败」(pageFailure 缺省 "throw"),不产出半套站点。
96
86
  const plan = await planSite(opts.input, opts.scan);
97
87
  await writeSite(plan, out);
@@ -1,19 +1,29 @@
1
1
  // HTTP server:把站点管线(site.ts 的 planSite)产出的同一份产物挂在 127.0.0.1 上按路径服务。
2
2
  // 这里不携带任何取数或布局知识——查不到清单条目就是 404,与 `--out` 写盘的文件逐字节一致
3
- // (docs/feature/reports/view.md 开篇;奇偶由 site-parity 测试守护)。宿主语义只有三条,全部
4
- // 作用在管线输入端:打开首页整份重建(数据永远是盘上最新)、单页渲染失败折成页内错误块
5
- // (pageFailure: "embed")、报告槽可被位置参数 / --experiment 收窄。
3
+ // (docs/feature/reports/view.md 开篇;奇偶由 site-parity 测试守护)。宿主语义只有两条,全部
4
+ // 作用在管线之外:打开首页整份重建(数据永远是盘上最新)、单页渲染失败折成页内错误块
5
+ // (pageFailure: "embed")。位置参数 / --exp 收窄是管线输入,不是宿主语义——两宿主同义。
6
6
 
7
7
  import { createServer, type Server } from "node:http";
8
- import { type ViewScanOptions } from "./data.ts";
9
- import { planSite, readSiteFile, type SitePlan } from "./site.ts";
8
+ import { loadViewScan, type ViewScanOptions } from "./data.ts";
9
+ import { planSite, readSiteFile, renderStandaloneAttemptDocument, type SitePlan } from "./site.ts";
10
10
  import { formatThrown } from "../util.ts";
11
+ import type { AttemptLocator } from "../results/locator.ts";
12
+
13
+ const HTML_TYPE = "text/html; charset=utf-8";
14
+
15
+ /** `attempt/<locator>.html` 站点路径 → 磁盘/清单键用的原始 locator(未编码,含字面 `@`;
16
+ * 见 site.ts「站点管线」对编码边界的说明)。不是这个形状返回 undefined。 */
17
+ function attemptLocatorFromSitePath(sitePath: string): AttemptLocator | undefined {
18
+ if (!sitePath.startsWith("attempt/") || !sitePath.endsWith(".html")) return undefined;
19
+ return sitePath.slice("attempt/".length, -".html".length) as AttemptLocator;
20
+ }
11
21
 
12
22
  export interface ViewOptions {
13
23
  input?: string;
14
24
  out?: string;
15
25
  port?: number;
16
- /** 报告槽的组合语义(位置前缀 / --experiment / --report),透传给站点管线。 */
26
+ /** 站点管线的组合语义(位置前缀 / --exp 收窄有效根,--report 换报告槽),透传给管线。 */
17
27
  scan?: ViewScanOptions;
18
28
  }
19
29
 
@@ -70,6 +80,21 @@ export async function startViewServer(opts: ViewOptions = {}): Promise<ViewServe
70
80
  file = plan.files.get(sitePath);
71
81
  }
72
82
  if (!file) {
83
+ // 本地宿主的 attempt 详情路由越过收窄,对完整结果根解析(docs/engineering/unit-tests/
84
+ // reports/cases.md 第 198/220 行;与 `show @<locator>` 同一套「各自结果根语义寻址」,
85
+ // 不是 SitePlan 清单之外的旁路取数——这条路由本来就不该受 --exp/eval 前缀收窄限制,
86
+ // 与「server 不提供清单之外的路径」的奇偶保证不冲突,那条保证只约束收窄之内的路径)。
87
+ const locator = attemptLocatorFromSitePath(sitePath);
88
+ if (locator !== undefined) {
89
+ const fullScan = await loadViewScan(input, { ...scanOptions, experiment: undefined, patterns: [] }).catch(() => undefined);
90
+ const handle = fullScan?.attemptPages?.locators.get(locator);
91
+ if (fullScan && handle) {
92
+ const body = await renderStandaloneAttemptDocument(fullScan, locator, handle);
93
+ res.writeHead(200, { "content-type": HTML_TYPE, "cache-control": "no-store" });
94
+ res.end(body);
95
+ return;
96
+ }
97
+ }
73
98
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
74
99
  res.end("not found");
75
100
  return;
@@ -80,6 +105,11 @@ export async function startViewServer(opts: ViewOptions = {}): Promise<ViewServe
80
105
  res.end("not found");
81
106
  return;
82
107
  }
108
+ // 同一路径同一 plan 生命周期内不重复求值(architecture.md「管线以 page 实例为单位执行」):
109
+ // lazy 产出器求值一次后把结果写回清单,下一次同路径请求(未触发 rebuild 之前)直接命中。
110
+ if (file.source.kind === "lazy") {
111
+ plan.files.set(sitePath, { ...file, source: { kind: "content", body: body as string } });
112
+ }
83
113
  res.writeHead(200, { "content-type": file.contentType, "cache-control": "no-store" });
84
114
  res.end(body);
85
115
  } catch (e) {
@@ -1,12 +1,13 @@
1
1
  // server(data.ts)与前端(app/)共用的 view 数据形状。
2
2
  // viewData 会被序列化进静态 HTML,两边必须对同一份声明编程;只允许 type import。
3
3
  //
4
- // viewData 只携带证据室与壳需要的东西:快照明细(locator / artifactBase 已注入)、
5
- // skipped、项目名与 run 元信息。统计口径(KPI / 榜单 / 挑选警告)整体住在报告槽的
6
- // 静态 HTML 里(ExperimentComparison 或 --report 的报告自己算),壳与报告之间没有第二条数据通道。
4
+ // viewData 只携带壳需要的东西:skipped、项目名与 run 元信息。attempt 明细不在这里——
5
+ // 每份 attempt/<locator>.html 是独立静态文档(site.ts),不通过 viewData 这条通道下发;
6
+ // 统计口径(KPI / 榜单 / 挑选警告)整体住在报告槽的静态 HTML 里(ExperimentComparison 或
7
+ // --report 的报告自己算),壳与报告之间没有第二条数据通道。
7
8
 
8
- import type { EvalResult, LocalizedText } from "../../types.ts";
9
- import type { ReportLocale } from "../../report/locale.ts";
9
+ import type { LocalizedText } from "../../types.ts";
10
+ import type { ReportLocale } from "../../report/model/locale.ts";
10
11
  import type { AttemptLocator } from "../../results/locator.ts";
11
12
 
12
13
  export type { AttemptLocator };
@@ -47,30 +48,6 @@ export interface ViewReportMeta {
47
48
  initialPageId: string;
48
49
  }
49
50
 
50
- /** view 侧的 attempt 结果 = 瘦身后的 EvalResult + loader 注入的深链身份(不透明 AttemptLocator,
51
- * `#/attempt/@<locator>` 路由的参数,与 Reports 的 MetricCell.refs / `ctx.attemptHref` 同一身份契约)
52
- * 与 artifact 基址。 */
53
- export type ViewEvalResult = EvalResult & { locator?: AttemptLocator };
54
-
55
- /**
56
- * 快照 = 单次跑的实验(experiment × 一次运行),与 niceeval/results 的 Snapshot 同口径。
57
- * 携带 attempt 明细供证据室(钻取 / AttemptModal / Runs / Traces)渲染;
58
- * 榜单统计不从这里算,吃 ViewData.table / overview 的官方产物。
59
- */
60
- export interface ViewSnapshot {
61
- experimentId: string;
62
- agent: string;
63
- model?: string;
64
- startedAt: string;
65
- /** 快照的根相对路径(= niceeval/results 的 AttemptRef.snapshot,两段:`<experiment-dir>/<snapshot-dir>`)。 */
66
- run: string;
67
- /** 是否为该实验在 results.latest() 口径下的最新一次快照 —— 证据室的 latest 标记,与报告槽 Selection
68
- (现刻水位,可能合成自更早快照)是两个独立概念,不要混用。 */
69
- latest: boolean;
70
- /** 该快照的 attempt 明细(跨快照去重后的幸存条目;locator / artifactBase 已注入)。 */
71
- results: ViewEvalResult[];
72
- }
73
-
74
51
  /**
75
52
  * 目录扫描里被跳过的 run 的结构化条目;三种原因与 niceeval/results 的 skipped 一致。
76
53
  * 页面上的呈现不走它:不可读快照已形成 `unreadable-snapshot` Scope warning,由报告页内的
@@ -99,8 +76,6 @@ export interface ViewData {
99
76
  lastRunAt?: string;
100
77
  /** 报告槽 Selection 合成自几个物理 run。 */
101
78
  composedRuns: number;
102
- /** 全部历史快照(跨快照按身份键去重后);attempt 详情路由对这份完整集合解析,不随报告 Scope 收窄。 */
103
- snapshots: ViewSnapshot[];
104
79
  /** 读不了的落盘(三种原因);呈现走报告页内的 ScopeWarnings(unreadable-snapshot warning)。 */
105
80
  skippedRuns?: SkippedRunNotice[];
106
81
  /** 报告外壳与页导航的声明(规范化后);缺省时前端按单页 `report` 兜底。 */