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
@@ -62,6 +62,10 @@ a { color: inherit; }
62
62
  display: flex;
63
63
  align-items: center;
64
64
  justify-content: space-between;
65
+ /* space-between 只在有富余空间时才撑开相邻项;.shell-links 的 margin-left:auto 会把富余
66
+ 空间全部吸收到它左侧,导致品牌与导航贴脸、shell-links 与语言切换贴脸。显式 gap 保证
67
+ 每一对相邻项(品牌—导航、shell-links—语言切换)恒有最小间距,不依赖富余空间。 */
68
+ gap: 28px;
65
69
  padding: 0 clamp(20px, 5vw, 80px);
66
70
  background: color-mix(in oklch, var(--bg), transparent 4%);
67
71
  position: sticky;
@@ -849,6 +853,7 @@ a.brand:hover { opacity: 0.75; }
849
853
  .gstat.good { color: var(--good); }
850
854
  .gstat.bad { color: var(--bad); }
851
855
  .gstat.warn { color: var(--warn); }
856
+ .gstat.na { color: var(--muted, #8a8f98); }
852
857
  .gsend { width: 13px; height: 13px; flex-shrink: 0; color: var(--blue); }
853
858
 
854
859
  .ctext { white-space: pre; color: var(--text); padding-left: 8px; }
@@ -866,6 +871,8 @@ a.brand:hover { opacity: 0.75; }
866
871
  .abadge.good { color: var(--good); border-color: color-mix(in oklch, var(--good), var(--line) 55%); background: color-mix(in oklch, var(--good), transparent 90%); }
867
872
  .abadge.bad { color: var(--bad); border-color: color-mix(in oklch, var(--bad), var(--line) 55%); background: color-mix(in oklch, var(--bad), transparent 90%); }
868
873
  .abadge.warn { color: var(--warn); border-color: color-mix(in oklch, var(--warn), var(--line) 55%); background: color-mix(in oklch, var(--warn), transparent 90%); }
874
+ /* unavailable:独立第三态(非红非绿),证据评不了 ≠ 失败 */
875
+ .abadge.na { color: var(--muted, #8a8f98); border-color: var(--line); background: color-mix(in oklch, var(--muted, #8a8f98), transparent 92%); }
869
876
  .abadge-th { opacity: 0.55; font-weight: 400; }
870
877
 
871
878
  /* 可点击行的显式提示:send 行一个 "reply" 药丸,断言行一个右侧展开箭头;hover 时变亮。 */
@@ -956,9 +963,229 @@ a.brand:hover { opacity: 0.75; }
956
963
  .code-orphans { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; }
957
964
  .code-orphans-head { font-size: 11px; color: var(--muted); margin-bottom: 8px; }
958
965
 
966
+ /* ── Attempt 详情(契约:docs/feature/reports/view.md「Attempt 详情」) ── */
967
+ .modal-sect {
968
+ font-size: 11px;
969
+ font-weight: 660;
970
+ letter-spacing: 0.06em;
971
+ text-transform: uppercase;
972
+ color: var(--muted);
973
+ margin-bottom: 6px;
974
+ }
975
+ /* 断言区:仅源码不可用时出现的 fallback 呈现面。 */
976
+ .attempt-asserts { margin-bottom: 14px; }
977
+ .attempt-asserts-open {
978
+ margin-top: 6px;
979
+ border: 1px solid var(--line);
980
+ border-radius: 10px;
981
+ background: var(--panel);
982
+ overflow: hidden;
983
+ }
984
+ /* 复用 CodeView 的 .assert-detail 排版,但脱离代码行上下文:去掉行内缩进与左缘。 */
985
+ .attempt-asserts .assert-detail { padding: 10px 12px; border-left: 0; background: transparent; }
986
+ /* 长 expected/received(整文件 dump)一律有界滚动:断言明细无论住在失败索引 fallback、
987
+ 源码行展开还是 NoSourceBody,都不把页面其余区块挤出视口。 */
988
+ .assert-reason { max-height: 180px; overflow-y: auto; }
989
+ /* 失败索引行:summary 一行是标题(徽章/名字/matcher/锚),明细收在行内一次展开。 */
990
+ .fail-row { border-bottom: 1px solid var(--line); }
991
+ .fail-row:last-child { border-bottom: 0; }
992
+ .fail-sum {
993
+ display: flex;
994
+ align-items: baseline;
995
+ flex-wrap: wrap;
996
+ gap: 4px 8px;
997
+ padding: 8px 12px;
998
+ cursor: pointer;
999
+ user-select: none;
1000
+ list-style: none;
1001
+ min-width: 0;
1002
+ }
1003
+ .fail-sum::-webkit-details-marker { display: none; }
1004
+ .fail-sum::before {
1005
+ content: "";
1006
+ width: 0;
1007
+ height: 0;
1008
+ border-left: 4px solid var(--soft);
1009
+ border-top: 3.5px solid transparent;
1010
+ border-bottom: 3.5px solid transparent;
1011
+ flex-shrink: 0;
1012
+ align-self: center;
1013
+ transition: transform 0.12s;
1014
+ }
1015
+ .fail-row[open] > .fail-sum::before { transform: rotate(90deg); }
1016
+ .fail-sum:hover { background: color-mix(in oklch, var(--panel), var(--line) 30%); }
1017
+ .fail-name {
1018
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1019
+ font-size: 12px;
1020
+ color: var(--text);
1021
+ overflow-wrap: anywhere;
1022
+ }
1023
+ .fail-matcher {
1024
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1025
+ font-size: 11.5px;
1026
+ color: var(--muted);
1027
+ overflow-wrap: anywhere;
1028
+ }
1029
+ .fail-body {
1030
+ display: flex;
1031
+ flex-direction: column;
1032
+ gap: 6px;
1033
+ padding: 2px 12px 10px 24px;
1034
+ }
1035
+ .assert-loc {
1036
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1037
+ font-size: 11px;
1038
+ color: var(--soft);
1039
+ margin-left: 8px;
1040
+ white-space: nowrap;
1041
+ }
1042
+ .asserts-passed { margin-top: 8px; }
1043
+ .asserts-passed > summary,
1044
+ .asserts-passed-group > summary {
1045
+ cursor: pointer;
1046
+ user-select: none;
1047
+ font-size: 12px;
1048
+ color: var(--muted);
1049
+ }
1050
+ .asserts-passed > summary:hover,
1051
+ .asserts-passed-group > summary:hover { color: var(--text); }
1052
+ .asserts-passed-group { margin: 6px 0 0 14px; }
1053
+ .asserts-passed-group > .assert-detail { margin-top: 4px; }
1054
+
1055
+ /* ── Attempt 详情:统一时间树(整区默认收合;展开后主链分解条 + 默认收合的 children) ── */
1056
+ .attempt-timing { margin: 14px 0; }
1057
+ summary.timing-sum {
1058
+ display: flex;
1059
+ align-items: center;
1060
+ gap: 8px;
1061
+ cursor: pointer;
1062
+ user-select: none;
1063
+ list-style: none;
1064
+ }
1065
+ summary.timing-sum::-webkit-details-marker { display: none; }
1066
+ summary.timing-sum::before {
1067
+ content: "";
1068
+ width: 0;
1069
+ height: 0;
1070
+ border-left: 4px solid var(--soft);
1071
+ border-top: 3.5px solid transparent;
1072
+ border-bottom: 3.5px solid transparent;
1073
+ flex-shrink: 0;
1074
+ transition: transform 0.12s;
1075
+ }
1076
+ details[open] > summary.timing-sum::before { transform: rotate(90deg); }
1077
+ summary.timing-sum:hover .modal-sect { color: var(--text); }
1078
+ .timing-sum .modal-sect { margin-bottom: 0; }
1079
+ .phase-bar {
1080
+ display: flex;
1081
+ height: 8px;
1082
+ border-radius: 4px;
1083
+ overflow: hidden;
1084
+ gap: 1px;
1085
+ margin: 7px 0 8px;
1086
+ }
1087
+ .phase-seg { min-width: 2px; flex-basis: 0; }
1088
+ .phase-seg-1 { background: var(--series-1); }
1089
+ .phase-seg-2 { background: var(--series-2); }
1090
+ .phase-seg-3 { background: var(--series-3); }
1091
+ .phase-seg-4 { background: var(--series-4); }
1092
+ .phase-seg-5 { background: var(--series-5); }
1093
+ .phase-seg-6 { background: var(--series-6); }
1094
+ .phase-seg-bad { background: var(--bad); }
1095
+ .phase-list,
1096
+ .timing-kids { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
1097
+ .timing-kids { padding-left: 18px; margin-top: 2px; }
1098
+ .phase-row,
1099
+ .tnode-row { display: flex; align-items: center; gap: 7px; font-size: 12px; line-height: 1.7; min-width: 0; }
1100
+ summary.phase-row,
1101
+ summary.tnode-row { cursor: pointer; user-select: none; list-style: none; }
1102
+ summary.phase-row::-webkit-details-marker,
1103
+ summary.tnode-row::-webkit-details-marker { display: none; }
1104
+ summary.phase-row::before,
1105
+ summary.tnode-row::before {
1106
+ content: "";
1107
+ width: 0;
1108
+ height: 0;
1109
+ border-left: 4px solid var(--soft);
1110
+ border-top: 3.5px solid transparent;
1111
+ border-bottom: 3.5px solid transparent;
1112
+ flex-shrink: 0;
1113
+ transition: transform 0.12s;
1114
+ }
1115
+ details[open] > summary.phase-row::before,
1116
+ details[open] > summary.tnode-row::before { transform: rotate(90deg); }
1117
+ /* 无 children 的行:占位对齐有箭头的行。 */
1118
+ li.phase-row::before,
1119
+ li.tnode-row::before { content: ""; width: 4px; flex-shrink: 0; }
1120
+ .phase-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
1121
+ .phase-dot-none { background: transparent; }
1122
+ /* 名称列定宽:各行的耗时起始对齐成一列(最长 phase 名 telemetry.configure 也在宽度内)。 */
1123
+ .phase-name { color: var(--text); white-space: nowrap; width: 168px; flex-shrink: 0; }
1124
+ .phase-dur { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
1125
+ .phase-fail { color: var(--bad); font-weight: 650; }
1126
+ .phase-kids {
1127
+ font-size: 10px;
1128
+ color: var(--soft);
1129
+ border: 1px solid var(--line);
1130
+ border-radius: 999px;
1131
+ padding: 0 6px;
1132
+ line-height: 1.5;
1133
+ }
1134
+ .tnode-label {
1135
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1136
+ font-size: 11.5px;
1137
+ color: var(--muted);
1138
+ overflow: hidden;
1139
+ text-overflow: ellipsis;
1140
+ white-space: nowrap;
1141
+ min-width: 0;
1142
+ }
1143
+ .tnode-meta { font-size: 11px; color: var(--soft); padding: 3px 0 3px 18px; }
1144
+ .tnode-trace { margin: 4px 0 6px 18px; }
1145
+ .phase-closing { margin-top: 8px; }
1146
+ .phase-closing-head { font-size: 11px; color: var(--soft); margin-bottom: 2px; }
1147
+
1148
+ /* usage / sandbox 元信息 chips。 */
1149
+ .meta-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
1150
+ .meta-chip {
1151
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1152
+ font-size: 11px;
1153
+ color: var(--muted);
1154
+ border: 1px solid var(--line);
1155
+ border-radius: 6px;
1156
+ padding: 3px 9px;
1157
+ background: var(--panel);
1158
+ }
1159
+ .meta-chip b { color: var(--text); font-weight: 640; }
1160
+
959
1161
  /* lightweight TS syntax tokens */
960
1162
  .tok-comment { color: var(--soft); font-style: italic; }
961
1163
  .tok-str { color: light-dark(#0a7d3c, #7ee787); }
962
1164
  .tok-kw { color: light-dark(#9333ea, #c792ea); }
963
1165
  .tok-num { color: light-dark(#b25000, #f0a36b); }
964
1166
  .tok-fn { color: light-dark(#0b62d6, #82aaff); }
1167
+
1168
+ /* ── 外壳:导航右侧外部链接与页脚(docs/feature/reports/library/shell.md) ── */
1169
+ .shell-links { display: flex; gap: 8px; align-items: center; margin-left: auto; }
1170
+ /* 链接自带 padding 撑大可点击区域(而非仅靠文字宽度);.shell-links 的 gap 相应收窄,
1171
+ 两条链接之间的视觉间距(padding + gap + padding)与改前基本持平。 */
1172
+ .shell-links a { color: var(--soft); text-decoration: none; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; padding: 6px 4px; }
1173
+ .shell-links a:hover { color: var(--text); }
1174
+ /* ReportLink.icon:内联 SVG 字标,渲染在 label 前(shell.md「字段穷尽」)。 */
1175
+ .shell-link-icon { display: inline-flex; align-items: center; }
1176
+ .shell-link-icon svg { width: 14px; height: 14px; display: block; fill: currentColor; }
1177
+ .site-footer {
1178
+ display: flex;
1179
+ gap: 12px;
1180
+ align-items: baseline;
1181
+ justify-content: center;
1182
+ flex-wrap: wrap;
1183
+ padding: 24px 24px 32px;
1184
+ color: var(--soft);
1185
+ font-size: 12px;
1186
+ }
1187
+ /* 品牌行:hero 之下、品牌蓝,恒带官网链接(shell.md「行为约束」)。 */
1188
+ .hero .powered-by { display: block; margin-top: 16px; font-size: 12px; }
1189
+ .hero .powered-by a { color: var(--blue); text-decoration: none; }
1190
+ .hero .powered-by a:hover { text-decoration: underline; }
1191
+ .nre-page-error pre { white-space: pre-wrap; }
@@ -1,9 +1,11 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
1
2
  // niceeval view 的报告槽与宿主组合语义(docs/feature/reports/architecture.md「Selection 是计算入口」
2
- // 与裁决记录 6;公开行为准绳 docs-site/zh/guides/viewing-results.mdx / custom-reports.mdx)。
3
+ // 与裁决记录 6;公开行为准绳 docs-site/zh/how-to/viewing-results.mdx / custom-reports.mdx)。
3
4
  // 覆盖:
4
- // - 组合语义与 show 对齐:位置前缀收窄报告槽 Selection、--experiment 过滤、匹配不到直说;
5
- // - 单文件模式共存:存在的文件路径 单文件模式,目录报错直说走 --run,其余按 eval 前缀;
6
- // - 报告槽恒在:裸跑填充 ExperimentComparison,--report 整槽替换;en / zh-CN 双语各渲染一遍;
5
+ // - 组合语义与 show 对齐:位置前缀收窄报告槽 Scope、--experiment 过滤、匹配不到直说;
6
+ // - 输入语义:位置参数只表示 eval id 前缀(不随文件系统状态改变),结果根走 --results,
7
+ // 单开一份快照走 --snapshot(文件不可读时失败);
8
+ // - 报告槽恒在:裸跑填充内建报告,--report 整槽替换;en / zh-CN 双语各渲染一遍;
7
9
  // 裸跑 ≡ --report <re-export ExperimentComparison 的文件>(等价性);
8
10
  // - --out 静态导出:index.html 含两个语言的报告块、官方样式与增强 runtime,报告块零 <script>;
9
11
  // - dev server 装载语义:报告文件变更 → 下次装载整页重算(mtime cache-busting)。
@@ -20,7 +22,8 @@ import { afterEach, describe, expect, it } from "vitest";
20
22
  // (see src/view/data.ts's comment) — a raw-src import would be a structurally-identical but
21
23
  // `instanceof`-incompatible class.
22
24
  import { ReportLoadError } from "../../dist/report/load.js";
23
- import { ViewInputError, loadViewScan } from "./data.ts";
25
+ import { ViewInputError, loadViewScan, type ViewScan } from "./data.ts";
26
+ import { renderHtml } from "./server.ts";
24
27
  import { buildView, resolveViewInput } from "./index.ts";
25
28
  import { runShow } from "../show/index.ts";
26
29
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
@@ -84,7 +87,7 @@ async function seedRoot(): Promise<string> {
84
87
  await writeSnapshot(root, "compare_bub", "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", agent: "bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
85
88
  res("weather/brooklyn", "passed"),
86
89
  res("fixtures/button", "failed", {
87
- assertions: [{ name: 'fileChanged("Button.tsx")', severity: "gate", score: 0, passed: false }],
90
+ assertions: [{ name: 'fileChanged("Button.tsx")', severity: "gate", score: 0, outcome: "failed" as const }],
88
91
  }),
89
92
  ]);
90
93
  await writeSnapshot(root, "compare_codex", "2026-07-09T10-00-00-000Z", { experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" }, [
@@ -95,47 +98,62 @@ async function seedRoot(): Promise<string> {
95
98
 
96
99
  // ───────────────────────── 位置参数语义(单文件模式共存) ─────────────────────────
97
100
 
98
- describe("resolveViewInput · 位置参数语义", () => {
99
- it("存在的文件路径 → 单文件模式;其余位置参数按 eval id 前缀", async () => {
101
+ describe("resolveViewInput · 输入语义", () => {
102
+ it("位置参数只表示 eval id 前缀:文件与目录路径不改变含义", async () => {
100
103
  const root = await seedRoot();
101
104
  const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
102
- expect(resolveViewInput(root, [file])).toEqual({ input: file, patterns: [] });
105
+ // 恰好是存在文件/目录的位置参数也照常当前缀(后续按「无匹配」报错,不做模糊猜测)
106
+ expect(resolveViewInput(root, [file])).toEqual({ patterns: [file] });
107
+ expect(resolveViewInput(root, ["compare_bub"])).toEqual({ patterns: ["compare_bub"] });
103
108
  expect(resolveViewInput(root, ["weather", "fixtures/button"])).toEqual({
104
109
  patterns: ["weather", "fixtures/button"],
105
110
  });
106
111
  expect(resolveViewInput(root, [])).toEqual({ patterns: [] });
107
112
  });
108
113
 
109
- it("目录位置参数:报错直说走 --run(位置参数留给 eval id 前缀)", async () => {
114
+ it("--snapshot 单开一份快照文件;文件不可读时失败(与扫描模式的跳过相反)", async () => {
110
115
  const root = await seedRoot();
111
- expect(() => resolveViewInput(root, ["compare_bub"])).toThrow(ViewInputError);
112
- expect(() => resolveViewInput(root, ["compare_bub"])).toThrow(/--run/);
116
+ const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
117
+ expect(resolveViewInput(root, [], { snapshot: file })).toEqual({ input: file, patterns: [] });
118
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "nope.json") })).toThrow(ViewInputError);
119
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "nope.json") })).toThrow(/--snapshot/);
120
+ // 目录不是快照文件。
121
+ expect(() => resolveViewInput(root, [], { snapshot: join(root, "compare_bub") })).toThrow(/--snapshot/);
113
122
  });
114
123
 
115
- it("单文件模式不与其它位置参数或 --run 混用,歧义报错直说", async () => {
124
+ it("--results 指向不存在的目录:直说", async () => {
116
125
  const root = await seedRoot();
117
- const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
118
- expect(() => resolveViewInput(root, [file, "weather"])).toThrow(/exactly one path/);
119
- expect(() => resolveViewInput(root, [file], root)).toThrow(/--run/);
126
+ expect(() => resolveViewInput(root, [], { results: join(root, "nope") })).toThrow(/Results directory not found/);
120
127
  });
121
128
 
122
- it("--run 指向不存在的目录:直说", async () => {
129
+ it("--results 换结果根,位置参数仍是前缀", async () => {
123
130
  const root = await seedRoot();
124
- expect(() => resolveViewInput(root, [], join(root, "nope"))).toThrow(/Results directory not found/);
131
+ expect(resolveViewInput("/elsewhere", ["weather"], { results: root })).toEqual({ input: root, patterns: ["weather"] });
125
132
  });
126
133
 
127
- it("--run 换结果根,位置参数仍是前缀", async () => {
134
+ it("--results 与 --snapshot 互斥:报错直说", async () => {
128
135
  const root = await seedRoot();
129
- expect(resolveViewInput("/elsewhere", ["weather"], root)).toEqual({ input: root, patterns: ["weather"] });
136
+ const file = join(root, "compare_bub", "2026-07-08T10-00-00-000Z", "snapshot.json");
137
+ expect(() => resolveViewInput(root, [], { results: root, snapshot: file })).toThrow(/mutually exclusive/);
130
138
  });
131
139
  });
132
140
 
141
+
142
+ /** 单页报告(裸跑 / 树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
143
+ function slotHtml(scan: ViewScan): { en: string; "zh-CN": string } {
144
+ expect(scan.reportPages).toHaveLength(1);
145
+ expect(scan.reportPages[0]!.id).toBe("report");
146
+ return scan.reportPages[0]!.html as { en: string; "zh-CN": string };
147
+ }
148
+
133
149
  // ───────────────────────── 组合语义(与 show 对齐) ─────────────────────────
134
150
 
135
151
  describe("loadViewScan · 组合语义", () => {
136
152
  it("位置前缀收窄报告槽 Selection;证据室快照不收窄,深链恒可达", async () => {
137
153
  const root = await seedRoot();
138
- const { viewData, reportHtml } = await loadViewScan(root, { patterns: ["weather"] });
154
+ const scan = await loadViewScan(root, { patterns: ["weather"] });
155
+ const { viewData } = scan;
156
+ const reportHtml = slotHtml(scan);
139
157
  // 报告槽:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
140
158
  expect(reportHtml.en).toContain("compare/bub");
141
159
  expect(reportHtml.en).toContain("compare/codex");
@@ -147,7 +165,7 @@ describe("loadViewScan · 组合语义", () => {
147
165
 
148
166
  it("--experiment 过滤:报告槽 Selection 只留该实验", async () => {
149
167
  const root = await seedRoot();
150
- const { reportHtml } = await loadViewScan(root, { experiment: "compare/codex" });
168
+ const reportHtml = slotHtml(await loadViewScan(root, { experiment: "compare/codex" }));
151
169
  expect(reportHtml.en).toContain("compare/codex");
152
170
  expect(reportHtml.en).not.toContain("compare/bub");
153
171
  });
@@ -161,7 +179,7 @@ describe("loadViewScan · 组合语义", () => {
161
179
 
162
180
  it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则)", async () => {
163
181
  const root = await seedRoot();
164
- const { reportHtml } = await loadViewScan(root);
182
+ const reportHtml = slotHtml(await loadViewScan(root));
165
183
  // 默认报告的榜单含两个实验;官方组件的稳定类名在场。
166
184
  expect(reportHtml.en).toContain("compare/bub");
167
185
  expect(reportHtml.en).toContain("compare/codex");
@@ -174,19 +192,21 @@ describe("loadViewScan · 组合语义", () => {
174
192
  describe("loadViewScan · 默认报告槽(裸跑)", () => {
175
193
  it("裸跑产出的报告槽 HTML 与 --report <re-export ExperimentComparison 的文件> 完全一致(双语)", async () => {
176
194
  const root = await seedRoot();
177
- const bare = await loadViewScan(root);
178
- const viaReport = await loadViewScan(root, {
179
- report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
180
- });
181
- expect(bare.reportHtml.en).toBe(viaReport.reportHtml.en);
182
- expect(bare.reportHtml["zh-CN"]).toBe(viaReport.reportHtml["zh-CN"]);
195
+ const bare = slotHtml(await loadViewScan(root));
196
+ const viaReport = slotHtml(
197
+ await loadViewScan(root, {
198
+ report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
199
+ }),
200
+ );
201
+ expect(bare.en).toBe(viaReport.en);
202
+ expect(bare["zh-CN"]).toBe(viaReport["zh-CN"]);
183
203
  });
184
204
 
185
205
  it("报告槽双语渲染:同一棵树按 locale 渲染两遍,chrome 文案分语言、数据不分语言", async () => {
186
206
  const root = await seedRoot();
187
- const { reportHtml } = await loadViewScan(root);
188
- expect(reportHtml.en).toContain("Pass rate"); // ExperimentList 主行(en)
189
- expect(reportHtml["zh-CN"]).toContain("成功率"); // ExperimentList 主行(zh-CN,passRate 的 zh label)
207
+ const reportHtml = slotHtml(await loadViewScan(root));
208
+ expect(reportHtml.en).toContain("End-to-end pass rate"); // ExperimentList 主行(en)
209
+ expect(reportHtml["zh-CN"]).toContain("端到端成功率"); // ExperimentList 主行(zh-CN)
190
210
  for (const html of [reportHtml.en, reportHtml["zh-CN"]]) {
191
211
  expect(html).toContain("compare/bub");
192
212
  // 失败案例深链进证据室:不透明 AttemptLocator 单段路由 `#/attempt/@<locator>`,
@@ -198,8 +218,9 @@ describe("loadViewScan · 默认报告槽(裸跑)", () => {
198
218
 
199
219
  it("失败清单与警告住在报告槽里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
200
220
  const root = await seedRoot();
201
- const { viewData, reportHtml } = await loadViewScan(root);
202
- expect(reportHtml.en).toContain("fixtures/button");
221
+ const scan = await loadViewScan(root);
222
+ const { viewData } = scan;
223
+ expect(slotHtml(scan).en).toContain("fixtures/button");
203
224
  // viewData 只有证据室数据:快照 + skipped + 壳元信息。
204
225
  expect(viewData).not.toHaveProperty("overview");
205
226
  expect(viewData).not.toHaveProperty("table");
@@ -213,14 +234,14 @@ describe("loadViewScan · --report 报告槽", () => {
213
234
  it("报告树渲染为静态 HTML:官方水位 + 自定义摆法 + <Style> 产物 + 证据室深链,零 <script>", async () => {
214
235
  const root = await seedRoot();
215
236
  const scan = await loadViewScan(root, { report: { path: EXAM_REPORT, cwd: root } });
216
- const html = scan.reportHtml.en;
237
+ const html = slotHtml(scan).en;
217
238
  expect(html).toContain("考试成绩单"); // 自定义 Section
218
239
  expect(html).toContain("nre-"); // 官方组件的稳定类名
219
240
  expect(html).toContain("<style>.exam-note { color: #4a7; }</style>"); // <Style> 随树带走
220
241
  expect(html).toMatch(/href="#\/attempt\/@[0-9a-z]+"/); // 失败案例深链进证据室(单段 locator 路由)
221
242
  expect(html).not.toContain("<script"); // 报告槽产物零客户端 JS,不 hydrate
222
243
  // 用户报告同样双语渲染两遍(壳按界面语言摆放)。
223
- expect(scan.reportHtml["zh-CN"]).toContain("考试成绩单");
244
+ expect(slotHtml(scan)["zh-CN"]).toContain("考试成绩单");
224
245
  // 证据室数据契约(__NICEEVAL_VIEW_DATA__)原样保留:快照、locator、skipped 不动。
225
246
  expect(scan.viewData.snapshots.length).toBeGreaterThan(0);
226
247
  expect(scan.viewData.snapshots.flatMap((s) => s.results).every((r) => r.locator)).toBe(true);
@@ -234,15 +255,16 @@ describe("loadViewScan · --report 报告槽", () => {
234
255
  report: { path: EXAM_REPORT, cwd: root },
235
256
  });
236
257
  // 范围外的失败(fixtures/button)不再出现在报告里;范围内的实验行都在。
237
- expect(scan.reportHtml.en).not.toContain("fixtures/button");
238
- expect(scan.reportHtml.en).toContain("compare/bub");
239
- expect(scan.reportHtml.en).toContain("compare/codex");
258
+ const html = slotHtml(scan);
259
+ expect(html.en).not.toContain("fixtures/button");
260
+ expect(html.en).toContain("compare/bub");
261
+ expect(html.en).toContain("compare/codex");
240
262
  });
241
263
 
242
264
  it("show --report 与 view --report 吃同一个报告文件,判定口径一致", async () => {
243
265
  const root = await seedRoot();
244
266
  let text = "";
245
- const code = await runShow(root, [], { run: root, report: EXAM_REPORT }, {
267
+ const code = await runShow(root, [], { results: root, report: EXAM_REPORT }, {
246
268
  out: (s) => (text += s),
247
269
  err: () => {},
248
270
  width: 120,
@@ -250,9 +272,10 @@ describe("loadViewScan · --report 报告槽", () => {
250
272
  expect(code).toBe(0);
251
273
  const scan = await loadViewScan(root, { report: { path: EXAM_REPORT, cwd: root } });
252
274
  // 同一棵树的两个面:同一份失败清单与同一个自定义 Section。
275
+ const html = slotHtml(scan);
253
276
  for (const needle of ["考试成绩单", "compare/bub", "compare/codex", "fixtures/button"]) {
254
277
  expect(text).toContain(needle);
255
- expect(scan.reportHtml.en).toContain(needle);
278
+ expect(html.en).toContain(needle);
256
279
  }
257
280
  });
258
281
 
@@ -269,6 +292,57 @@ describe("loadViewScan · --report 报告槽", () => {
269
292
  });
270
293
  });
271
294
 
295
+ // ───────────────────── 外壳:title 落点(hero / <title>)与 ReportLink.icon ─────────────────────
296
+
297
+ describe("loadViewScan · 外壳标题与 ReportLink.icon", () => {
298
+ /** 不经包入口也合法的最小外壳报告(与下方 reportSource 同一姿势):声明 title 与带 icon 的 link。 */
299
+ function shellReportSource(): string {
300
+ return [
301
+ 'const FACES = Symbol.for("niceeval.report.faces");',
302
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
303
+ "const Block = (props) => Block[FACES].web(props);",
304
+ "Block[FACES] = {",
305
+ ' web: () => "SHELL_BODY",',
306
+ ' text: () => "SHELL_BODY",',
307
+ "};",
308
+ "const definition = {",
309
+ ' kind: "report",',
310
+ ' title: { en: "Memory Evals", "zh-CN": "记忆能力评测" },',
311
+ ' links: [{ label: "GitHub", href: "https://example.com", icon: { svg: "<svg data-mark></svg>" } }],',
312
+ " scripts: [],",
313
+ " styles: [],",
314
+ ' pages: [{ id: "report", title: "Report", content: { $$typeof: Symbol.for("react.transitional.element"), type: Block, props: {}, key: null } }],',
315
+ "};",
316
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
317
+ "export default definition;",
318
+ "",
319
+ ].join("\n");
320
+ }
321
+
322
+ it("def.title 进 viewData.report.title,初始 <title> 同源烘进 HTML;link 的 icon svg 原样透传", async () => {
323
+ const root = await seedRoot();
324
+ const path = join(root, "shell-report.mjs");
325
+ await writeFile(path, shellReportSource(), "utf-8");
326
+ const scan = await loadViewScan(root, { report: { path, cwd: root } });
327
+ // 标题回退链第一级:def.title 原样(hero 与浏览器标题都吃它)。
328
+ expect(scan.viewData.report?.title).toEqual({ en: "Memory Evals", "zh-CN": "记忆能力评测" });
329
+ // ReportLink.icon 经 viewData 序列化边界原样透传(web 面在 label 前渲染,静态导出原样内联)。
330
+ expect(scan.viewData.report?.links).toEqual([
331
+ { label: "GitHub", href: "https://example.com", icon: { svg: "<svg data-mark></svg>" } },
332
+ ]);
333
+ const html = await renderHtml(scan);
334
+ expect(html).toContain("<title>Memory Evals</title>");
335
+ });
336
+
337
+ it("无 def.title 且快照无 name:标题落内置文案「Eval 运行结果 / Eval Results」,不再是产品名", async () => {
338
+ const root = await seedRoot(); // seedRoot 的快照都没有 name
339
+ const scan = await loadViewScan(root);
340
+ expect(scan.viewData.report?.title).toEqual({ en: "Eval Results", "zh-CN": "Eval 运行结果" });
341
+ const html = await renderHtml(scan);
342
+ expect(html).toContain("<title>Eval Results</title>");
343
+ });
344
+ });
345
+
272
346
  // ───────────────────────── dev server 装载语义:整页重算 ─────────────────────────
273
347
 
274
348
  describe("loadViewScan · 报告文件变更整页重算", () => {
@@ -276,20 +350,21 @@ describe("loadViewScan · 报告文件变更整页重算", () => {
276
350
  function reportSource(marker: string): string {
277
351
  return [
278
352
  'const FACES = Symbol.for("niceeval.report.faces");',
353
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
279
354
  "const Block = (props) => Block[FACES].web(props);",
280
355
  "Block[FACES] = {",
281
356
  ` web: () => "${marker}",`,
282
357
  ` text: () => "${marker}",`,
283
358
  "};",
284
- "export default {",
285
- ' [Symbol.for("niceeval.report.definition")]: true,',
286
- " build: () => ({",
287
- ' $$typeof: Symbol.for("react.transitional.element"),',
288
- " type: Block,",
289
- " props: {},",
290
- " key: null,",
291
- " }),",
359
+ "const definition = {",
360
+ ' kind: "report",',
361
+ " links: [],",
362
+ " scripts: [],",
363
+ " styles: [],",
364
+ ' pages: [{ id: "report", title: "Report", content: { $$typeof: Symbol.for("react.transitional.element"), type: Block, props: {}, key: null } }],',
292
365
  "};",
366
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
367
+ "export default definition;",
293
368
  "",
294
369
  ].join("\n");
295
370
  }
@@ -298,16 +373,16 @@ describe("loadViewScan · 报告文件变更整页重算", () => {
298
373
  const root = await seedRoot();
299
374
  const path = join(root, "report.mjs");
300
375
  await writeFile(path, reportSource("FIRST_RENDER"), "utf-8");
301
- const first = await loadViewScan(root, { report: { path, cwd: root } });
302
- expect(first.reportHtml.en).toContain("FIRST_RENDER");
376
+ const first = slotHtml(await loadViewScan(root, { report: { path, cwd: root } }));
377
+ expect(first.en).toContain("FIRST_RENDER");
303
378
 
304
379
  await writeFile(path, reportSource("SECOND_RENDER"), "utf-8");
305
380
  // mtime 精度兜底:显式把 mtime 拨到未来,确保与首次装载可区分。
306
381
  const future = new Date(Date.now() + 5000);
307
382
  await utimes(path, future, future);
308
- const second = await loadViewScan(root, { report: { path, cwd: root } });
309
- expect(second.reportHtml.en).toContain("SECOND_RENDER");
310
- expect(second.reportHtml.en).not.toContain("FIRST_RENDER");
383
+ const second = slotHtml(await loadViewScan(root, { report: { path, cwd: root } }));
384
+ expect(second.en).toContain("SECOND_RENDER");
385
+ expect(second.en).not.toContain("FIRST_RENDER");
311
386
  });
312
387
  });
313
388
 
@@ -323,16 +398,16 @@ describe("buildView · --out 与 --report", () => {
323
398
  await writeFile(join(artifactDir, "events.json"), "[]", "utf-8");
324
399
 
325
400
  const out = join(root, "site");
326
- await buildView({ input: root, out, scan: { report: { path: EXAM_REPORT, cwd: root } } });
401
+ await buildView({ input: root, out, allowSensitiveArtifacts: true, scan: { report: { path: EXAM_REPORT, cwd: root } } });
327
402
 
328
403
  const html = await readFile(join(out, "index.html"), "utf-8");
329
404
  // 双语两个 <template> 静态块都在,壳按界面语言摆放。
330
- expect(html).toContain('<template id="niceeval-report-en">');
331
- expect(html).toContain('<template id="niceeval-report-zh-CN">');
405
+ expect(html).toContain('<template id="niceeval-report-report-en">');
406
+ expect(html).toContain('<template id="niceeval-report-report-zh-CN">');
332
407
  expect(html).toContain("考试成绩单");
333
408
  expect(html).toContain("nre-"); // 官方组件样式(report/react/styles.css)随页注入
334
409
  // 报告块本体零 <script>:静态块起于 <template>,内部只有标记与 <style>。
335
- const block = html.split('<template id="niceeval-report-en">')[1]!.split("</template>")[0]!;
410
+ const block = html.split('<template id="niceeval-report-report-en">')[1]!.split("</template>")[0]!;
336
411
  expect(block).not.toContain("<script");
337
412
  // 证据室同站: artifact 按 /artifact/<base>/ 布局复制。
338
413
  expect(
@@ -340,17 +415,47 @@ describe("buildView · --out 与 --report", () => {
340
415
  ).toBe(true);
341
416
  });
342
417
 
418
+ it("--out 无档位:diff.json 有就带,o11y.json 永不复制", async () => {
419
+ const root = await seedRoot();
420
+ const artifactDir = join(root, "compare_codex", "2026-07-09T10-00-00-000Z", "weather", "brooklyn", "a0");
421
+ await mkdir(artifactDir, { recursive: true });
422
+ await writeFile(join(artifactDir, "events.json"), "[]", "utf-8");
423
+ await writeFile(join(artifactDir, "diff.json"), '{"windows":[]}', "utf-8");
424
+ await writeFile(join(artifactDir, "o11y.json"), "{}", "utf-8");
425
+
426
+ const out = join(root, "site");
427
+ await buildView({ input: root, out, allowSensitiveArtifacts: true });
428
+ const exported = join(out, "artifact", "compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a0");
429
+ expect(existsSync(join(exported, "diff.json"))).toBe(true);
430
+ expect(existsSync(join(exported, "events.json"))).toBe(true);
431
+ expect(existsSync(join(exported, "o11y.json"))).toBe(false);
432
+ });
433
+
434
+ it("--out 与位置参数 / --experiment 互斥:报错含 copySnapshots + filter 下一步", async () => {
435
+ const root = await seedRoot();
436
+ const out = join(root, "site");
437
+ for (const scan of [{ patterns: ["weather"] }, { experiment: "compare/bub" }]) {
438
+ const attempt = buildView({ input: root, out, allowSensitiveArtifacts: true, scan });
439
+ await expect(attempt).rejects.toBeInstanceOf(ViewInputError);
440
+ await expect(
441
+ buildView({ input: root, out, allowSensitiveArtifacts: true, scan }),
442
+ ).rejects.toThrow(/copySnapshots/);
443
+ }
444
+ // 同参数不带 --out 时照常收窄报告槽(不报错)。
445
+ await expect(loadViewScan(root, { patterns: ["weather"] })).resolves.toBeTruthy();
446
+ });
447
+
343
448
  it("默认导出(无 --report):报告槽填充 ExperimentComparison,双语块与增强 runtime 恒内联", async () => {
344
449
  const root = await seedRoot();
345
450
  const out = join(root, "site");
346
- await buildView({ input: root, out });
451
+ await buildView({ input: root, out, allowSensitiveArtifacts: true });
347
452
  const html = await readFile(join(out, "index.html"), "utf-8");
348
- expect(html).toContain('<template id="niceeval-report-en">');
349
- expect(html).toContain('<template id="niceeval-report-zh-CN">');
453
+ expect(html).toContain('<template id="niceeval-report-report-en">');
454
+ expect(html).toContain('<template id="niceeval-report-report-zh-CN">');
350
455
  expect(html).toContain("nre-"); // 官方组件的稳定类名(KPI / 榜单 / 散点来自 nre 组件)
351
456
  // 渐进增强 runtime(排序 / 过滤 / tooltip)恒内联;报告块本体仍零 <script>。
352
457
  expect(html).toContain("__nreEnhanced");
353
- const block = html.split('<template id="niceeval-report-en">')[1]!.split("</template>")[0]!;
458
+ const block = html.split('<template id="niceeval-report-report-en">')[1]!.split("</template>")[0]!;
354
459
  expect(block).not.toContain("<script");
355
460
  });
356
461
  });