niceeval 0.9.1 → 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 +14 -6
  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} +43 -11
  118. package/dist/report/{tree.js → definition/tree.js} +9 -11
  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 +11 -1
  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 +14 -0
  154. package/dist/shared/aggregate.js +31 -0
  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 +40 -16
  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 +6 -6
  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 +49 -49
  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 +71 -57
  224. package/src/context/types.ts +21 -21
  225. package/src/define.ts +13 -0
  226. package/src/i18n/en.ts +27 -12
  227. package/src/i18n/zh-CN.ts +27 -12
  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 +124 -106
  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} +56 -20
  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 +53 -2
  314. package/src/results/select.ts +29 -5
  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 +29 -0
  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 +56 -20
  356. package/src/show/render.ts +15 -239
  357. package/src/show/show.test.ts +127 -34
  358. package/src/view/app/App.test.tsx +1 -1
  359. package/src/view/app/App.tsx +119 -47
  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 +105 -83
  370. package/src/view/index.ts +1 -1
  371. package/src/view/server.ts +32 -2
  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 +0 -805
  376. package/src/view/view-report.test.ts +141 -22
  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
@@ -1,15 +1,19 @@
1
- // 排版原语 Row / Col / Section / Text / Style / Tabs / Tab / Table:八个内置双面组件,
2
- // 没有特殊机制(docs/feature/reports/library/layout.md)。web 面是普通 React 渲染;
1
+ // 排版原语 Row / Col / Grid / Section / Stat / Text / Style / Tabs / Tab / Table:十个内置
2
+ // 双面组件,没有特殊机制(docs/feature/reports/library/layout.md)。web 面是普通 React 渲染;
3
3
  // text 面用 ctx.render(child, 子宽) 显式传宽。Style 注入页级全局 CSS(树位置只决定声明
4
4
  // 顺序),text 面渲染为空。Table 是自定义表的标准件,官方表状组件的 text 面也建在它上面。
5
+ // Grid / Stat 的语义层(normalizeGrid 展平校验、text 面的 TextGridPlan 排版)在
6
+ // ./grid-layout.ts(docs/feature/reports/architecture.md「排版原语的语义层与面内布局」);
7
+ // 本文件只声明两面适配,不重复那份算术。
5
8
 
6
- import type { ReactNode } from "react";
7
- import type { AttemptLocator } from "../results/locator.ts";
9
+ import type { CSSProperties, ReactNode } from "react";
10
+ import type { AttemptLocator } from "../../results/locator.ts";
8
11
  import { COMPONENT_RAW_CHILDREN, COMPONENT_ROLE, defineComponent, type ReportNode } from "./tree.ts";
9
- import { localeText, resolveLocalizedText, type LocalizedText, type ReportLocale } from "./locale.ts";
10
- import { indentBlock, joinColumns, stringWidth, wrapDisplay } from "./text/layout.ts";
11
- import type { ColumnAlign } from "./text/layout.ts";
12
- import { renderTableText } from "./text/table.ts";
12
+ import { localeText, resolveLocalizedText, type LocalizedText, type ReportLocale } from "../model/locale.ts";
13
+ import { indentBlock, joinColumns, padDisplay, stringWidth, wrapDisplay } from "../model/text-layout.ts";
14
+ import type { ColumnAlign } from "../model/text-layout.ts";
15
+ import { renderTableText } from "./table-text.ts";
16
+ import { normalizeGrid, planTextGrid, type GridDensity, type GridVariant } from "./grid-layout.ts";
13
17
 
14
18
  function childArray(children: ReportNode): ReportNode[] {
15
19
  if (children === null || children === undefined || typeof children === "boolean") return [];
@@ -20,6 +24,9 @@ function cx(...parts: (string | undefined)[]): string {
20
24
  return parts.filter(Boolean).join(" ");
21
25
  }
22
26
 
27
+ /** `null`/缺数据的统一显示符:Table 与 Stat 共用,不补成 0。 */
28
+ const MISSING_MARK = "—";
29
+
23
30
  export interface LayoutProps {
24
31
  children?: ReportNode;
25
32
  className?: string;
@@ -76,27 +83,168 @@ export const Row = defineComponent<RowProps>({
76
83
  });
77
84
  Row.displayName = "Row";
78
85
 
86
+ // ───────────────────────── Grid / Stat ─────────────────────────
87
+
88
+ export interface GridProps extends LayoutProps {
89
+ /** 宽面最多摆几列;必须是有限正整数,运行时校验(docs/feature/reports/library/layout.md「Grid 与 Stat」)。 */
90
+ columns: number;
91
+ /** plain 无框;boxed 给每个 cell 完整四边框。默认 plain。 */
92
+ variant?: GridVariant;
93
+ /** 改变格内留白,并调整内置 Stat 的主值字号;不改变内容和分组。默认 regular。 */
94
+ density?: GridDensity;
95
+ }
96
+
97
+ /** boxed 单个 cell 的完整边框:同一份 TextGridPlan 决定内容宽度,四边不因换行残缺。 */
98
+ function boxCellBlock(content: string, contentWidth: number): string {
99
+ const lines = content.length > 0 ? content.split("\n") : [""];
100
+ const top = `┌${"─".repeat(contentWidth + 2)}┐`;
101
+ const bottom = `└${"─".repeat(contentWidth + 2)}┘`;
102
+ const body = lines.map((line) => `│ ${padDisplay(line, contentWidth)} │`);
103
+ return [top, ...body, bottom].join("\n");
104
+ }
105
+
106
+ /** 一个物理行内的 cell 并排:顶对齐、短 block 补空行到同高(joinColumns 已有语义)。 */
107
+ function renderGridRow(blocks: string[], contentWidths: number[], gutter: number, variant: GridVariant): string {
108
+ const separator = " ".repeat(gutter);
109
+ if (variant === "boxed") {
110
+ const boxed = blocks.map((block, i) => boxCellBlock(block, contentWidths[i]));
111
+ const boxWidths = contentWidths.map((w) => w + 4);
112
+ return joinColumns(boxed, boxWidths, separator);
113
+ }
114
+ return joinColumns(blocks, contentWidths, separator);
115
+ }
116
+
117
+ /**
118
+ * 自由摘要面板的格子容器:只负责呈现,不读取 Scope、不聚合 Metric。每个直接子节点
119
+ * (数组 / Fragment 先按 ReportNode 规则展平,空分支不占格)是一格;`Col` 把多个区块
120
+ * 归成一格。展平与 text 面排版的算术在 ./grid-layout.ts,这里只做两面结构适配。
121
+ */
122
+ export const Grid = defineComponent<GridProps>({
123
+ web({ children, columns, variant, density, className }) {
124
+ const normalized = normalizeGrid({ children, columns, variant, density });
125
+ const style: CSSProperties = { "--nre-grid-max-columns": normalized.columns } as CSSProperties;
126
+ return (
127
+ <div
128
+ className={cx("nre", "nre-grid", `nre-grid--${normalized.variant}`, `nre-grid--${normalized.density}`, className)}
129
+ style={style}
130
+ >
131
+ {normalized.cells.map((cell) => (
132
+ <div className="nre-grid-cell" key={cell.key}>
133
+ {cell.node as ReactNode}
134
+ </div>
135
+ ))}
136
+ </div>
137
+ );
138
+ },
139
+ text({ children, columns, variant, density }, ctx) {
140
+ const normalized = normalizeGrid({ children, columns, variant, density });
141
+ if (normalized.cells.length === 0) return "";
142
+ const plan = planTextGrid({
143
+ availableWidth: ctx.width,
144
+ cellCount: normalized.cells.length,
145
+ columns: normalized.columns,
146
+ density: normalized.density,
147
+ });
148
+ // 确定计划后才对每个 cell 调用一次 ctx.render——不为试探列数重复渲染。
149
+ const blocks = normalized.cells.map((cell, i) => ctx.render(cell.node, plan.contentWidths[i % plan.columns]));
150
+ const rows: string[] = [];
151
+ for (let start = 0; start < blocks.length; start += plan.columns) {
152
+ const rowBlocks = blocks.slice(start, start + plan.columns);
153
+ const rowWidths = plan.contentWidths.slice(0, rowBlocks.length);
154
+ rows.push(renderGridRow(rowBlocks, rowWidths, plan.gutter, normalized.variant));
155
+ }
156
+ return rows.join("\n\n");
157
+ },
158
+ });
159
+ Grid.displayName = "Grid";
160
+
161
+ export type StatTone = "neutral" | "positive" | "negative" | "warning";
162
+
163
+ export interface StatProps {
164
+ label: LocalizedText;
165
+ /** 已格式化的主值;null 明确渲染为 —,不补成 0。 */
166
+ value: LocalizedText | number | null;
167
+ /** 主值下面的短解释;省略时不留空行。 */
168
+ detail?: LocalizedText;
169
+ /** 主值的语义色;不从正负号、单位或 Metric.better 猜。默认 neutral。 */
170
+ tone?: StatTone;
171
+ className?: string;
172
+ }
173
+
174
+ /**
175
+ * Grid / Stat 共享的显示值规范化:LocalizedText 走 resolveLocalizedText,number 走当前
176
+ * locale 的 Intl.NumberFormat,null 变 —;两个面调同一份,不各写一套。
177
+ */
178
+ function resolveStatDisplay(value: LocalizedText | number | null, locale: ReportLocale): string {
179
+ if (value === null) return MISSING_MARK;
180
+ if (typeof value === "number") return new Intl.NumberFormat(locale).format(value);
181
+ return resolveLocalizedText(value, locale);
182
+ }
183
+
184
+ /** label / 主值 / 辅助信息的最小内容单元;可以脱离 Grid 单独使用。 */
185
+ export const Stat = defineComponent<StatProps>({
186
+ web({ label, value, detail, tone = "neutral", className }, ctx) {
187
+ return (
188
+ <div className={cx("nre", "nre-stat", `nre-stat--${tone}`, className)}>
189
+ <div className="nre-stat-label">{resolveLocalizedText(label, ctx.locale)}</div>
190
+ <div className="nre-stat-value">{resolveStatDisplay(value, ctx.locale)}</div>
191
+ {detail !== undefined ? <div className="nre-stat-detail">{resolveLocalizedText(detail, ctx.locale)}</div> : null}
192
+ </div>
193
+ );
194
+ },
195
+ text({ label, value, detail }, ctx) {
196
+ const lines = [resolveLocalizedText(label, ctx.locale), resolveStatDisplay(value, ctx.locale)];
197
+ if (detail !== undefined) lines.push(resolveLocalizedText(detail, ctx.locale));
198
+ return lines.flatMap((line) => wrapDisplay(line, ctx.width)).join("\n");
199
+ },
200
+ });
201
+ Stat.displayName = "Stat";
202
+
203
+ // ───────────────────────── Section ─────────────────────────
204
+
79
205
  export interface SectionProps extends LayoutProps {
80
206
  title: LocalizedText;
207
+ /** 标题行右侧的短元信息;text 面与标题同一行,空间不足时换到下一行。 */
208
+ meta?: LocalizedText;
209
+ }
210
+
211
+ /** 标题行右侧同一行放不下 meta 时,退化成标题后两格缩进折行。 */
212
+ function sectionHeadingLine(heading: string, metaText: string | undefined, width: number): string {
213
+ if (metaText === undefined) return heading;
214
+ const gap = width - stringWidth(heading) - stringWidth(metaText);
215
+ if (gap >= 1) return heading + " ".repeat(gap) + metaText;
216
+ const wrapped = wrapDisplay(metaText, Math.max(1, width - 2)).join("\n");
217
+ return `${heading}\n${indentBlock(wrapped, " ")}`;
81
218
  }
82
219
 
83
- /** 带标题的块:网页是标题层级,终端是标题行加缩进。 */
220
+ /** 带标题的块:网页是标题层级(可选 meta 同行右对齐),终端是标题行加缩进。 */
84
221
  export const Section = defineComponent<SectionProps>({
85
- web({ title, children, className }, ctx) {
222
+ web({ title, meta, children, className }, ctx) {
223
+ const titleText = resolveLocalizedText(title, ctx.locale);
224
+ const metaText = meta !== undefined ? resolveLocalizedText(meta, ctx.locale) : undefined;
86
225
  return (
87
226
  <section className={cx("nre", "nre-section", className)}>
88
- <h2 className="nre-section-title">{resolveLocalizedText(title, ctx.locale)}</h2>
227
+ {metaText !== undefined ? (
228
+ <header className="nre-section-header">
229
+ <h2 className="nre-section-title">{titleText}</h2>
230
+ <p className="nre-section-meta">{metaText}</p>
231
+ </header>
232
+ ) : (
233
+ <h2 className="nre-section-title">{titleText}</h2>
234
+ )}
89
235
  {children as ReactNode}
90
236
  </section>
91
237
  );
92
238
  },
93
- text({ title, children }, ctx) {
239
+ text({ title, meta, children }, ctx) {
94
240
  const heading = resolveLocalizedText(title, ctx.locale);
241
+ const metaText = meta !== undefined ? resolveLocalizedText(meta, ctx.locale) : undefined;
242
+ const headingLine = sectionHeadingLine(heading, metaText, ctx.width);
95
243
  const body = childArray(children)
96
244
  .map((child) => ctx.render(child, ctx.width - 2))
97
245
  .filter((block) => block.length > 0)
98
246
  .join("\n\n");
99
- return body.length > 0 ? `${heading}\n${indentBlock(body, " ")}` : heading;
247
+ return body.length > 0 ? `${headingLine}\n${indentBlock(body, " ")}` : headingLine;
100
248
  },
101
249
  });
102
250
  Section.displayName = "Section";
@@ -269,8 +417,6 @@ export interface TableProps {
269
417
  className?: string;
270
418
  }
271
419
 
272
- const MISSING_MARK = "—";
273
-
274
420
  /** 列 key 唯一、行 cells 不携带未声明 key、空列拒绝——无类型 JS 输入在渲染前同样校验。 */
275
421
  function validateTableProps(props: TableProps): void {
276
422
  if (!Array.isArray(props.columns) || props.columns.length === 0) {
@@ -344,9 +490,13 @@ export const Table = defineComponent<TableProps>({
344
490
  {hasLocator ? (
345
491
  <td>
346
492
  {row.locator ? (
347
- <a className="nre-locator" href={ctx.attemptHref(row.locator)}>
348
- {row.locator}
349
- </a>
493
+ ctx.attemptHref ? (
494
+ <a className="nre-locator" href={ctx.attemptHref(row.locator)}>
495
+ {row.locator}
496
+ </a>
497
+ ) : (
498
+ <span className="nre-locator">{row.locator}</span>
499
+ )
350
500
  ) : (
351
501
  <span className="nre-missing">{MISSING_MARK}</span>
352
502
  )}
@@ -5,26 +5,13 @@
5
5
  // `extends` 在另一份报告上叠外壳——页归 base、外壳逐字段覆盖,合并在调用时折叠完成,
6
6
  // 宿主装载看到的永远是已折叠的普通产物。
7
7
  //
8
- // renderReportToText text 宿主(show)的装载入口;web 宿主(view)的
9
- // renderReportToStaticHtml ./web.ts(那一侧才 import react-dom)。管线以页为单位执行:
10
- // 装载(规范化 + 静态校验)→ resolve → validate → render。
11
-
12
- import type { Results, Scope } from "../results/types.ts";
13
- import {
14
- createTextContext,
15
- renderNodeToText,
16
- resolveReportTree,
17
- validateReportTree,
18
- ResolveMemo,
19
- type ReportNode,
20
- type TextRenderOptions,
21
- } from "./tree.ts";
22
- import {
23
- localizedTextEquals,
24
- resolveLocalizedText,
25
- type LocalizedText,
26
- type ReportLocale,
27
- } from "./locale.ts";
8
+ // text/web 两个宿主的渲染入口(renderReportToText / renderReportToStaticHtml) ../runtime/
9
+ // (text.ts / web.ts);这里只有 ReportDefinition 的类型体系、装载规范化与元数据折叠
10
+ // (buildReportMeta / resolveReportTitle),不做任何渲染。
11
+
12
+ import type { Scope } from "../../results/types.ts";
13
+ import type { ReportNode } from "./tree.ts";
14
+ import { localizedTextEquals, type LocalizedText } from "../model/locale.ts";
28
15
 
29
16
  // ───────────────────────── 公开形状 ─────────────────────────
30
17
 
@@ -73,7 +60,7 @@ export interface ReportShell {
73
60
 
74
61
  export type NonEmptyArray<T> = readonly [T, ...T[]];
75
62
 
76
- export interface ReportPage {
63
+ export interface ReportPageBase {
77
64
  /** 页面身份:`--page <id>` 的取值、web 路由 `#/page/<id>` 与导航锚。小写字母、数字与连字符。 */
78
65
  id: string;
79
66
  /** 导航中的页名。 */
@@ -82,6 +69,18 @@ export interface ReportPage {
82
69
  content: ReportNode;
83
70
  }
84
71
 
72
+ /**
73
+ * 页按输入分两种形态,仍是同一个类型族,走同一条 resolve → validate → render 管线
74
+ * (docs/feature/reports/architecture.md「Attempt 详情是一张参数化 page」):
75
+ * - `input` 省略或为 `"scope"`:消费宿主选择的 Scope;省略时规范化为 `navigation: true`。
76
+ * - `input: "attempt"`:以 locator 为参数,消费一份 AttemptEvidence;没有 locator 时不能打开,
77
+ * 必须显式 `navigation: false`,不进导航。
78
+ * 一份报告至多声明一张 attempt-input page。
79
+ */
80
+ export type ReportPage =
81
+ | (ReportPageBase & { input?: "scope"; navigation?: boolean })
82
+ | (ReportPageBase & { input: "attempt"; navigation: false });
83
+
85
84
  /** content / pages / extends 三选一由类型表达,不把非法状态留到运行期。 */
86
85
  export type ReportDef = ReportShell &
87
86
  (
@@ -92,7 +91,7 @@ export type ReportDef = ReportShell &
92
91
  extends?: never;
93
92
  }
94
93
  | {
95
- /** 非空页列表;导航按数组顺序显示。 */
94
+ /** 非空页列表;`navigation !== false` 的项按数组顺序显示。 */
96
95
  pages: NonEmptyArray<ReportPage>;
97
96
  content?: never;
98
97
  extends?: never;
@@ -128,17 +127,27 @@ export interface ReportDefinition {
128
127
  readonly pages: NonEmptyArray<ReportPage>;
129
128
  }
130
129
 
131
- /** 规范化后的报告声明,经组合组件 ctx.report 只读可见(scripts / styles 是注入资产,不进)。 */
130
+ /** 规范化后页列表在 ctx.report 上的元数据形态(id / 导航页名 / 输入声明 / 导航资格)。 */
131
+ export interface ReportMetaPage {
132
+ id: string;
133
+ title: LocalizedText;
134
+ input: "scope" | "attempt";
135
+ navigation: boolean;
136
+ }
137
+
138
+ /**
139
+ * 规范化后的报告声明,经组合组件 ctx.report 只读可见(scripts / styles 是注入资产,不进)。
140
+ * 不携带"当前是哪一页"——那由 ctx.page(PageContext)表达,两者不是同一份状态
141
+ * (docs/feature/reports/library/shell.md「行为约束」)。
142
+ */
132
143
  export interface ReportMeta {
133
144
  /** 走完回退链(声明 title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」)后的标题。 */
134
145
  title: LocalizedText;
135
146
  /** 页头外链;声明省略时为空数组。 */
136
147
  links: readonly ReportLink[];
137
148
  footer?: LocalizedText;
138
- /** 规范化后的页列表(id 与导航页名),恒非空。 */
139
- pages: NonEmptyArray<{ id: string; title: LocalizedText }>;
140
- /** 当前渲染中的页 id。 */
141
- pageId: string;
149
+ /** 规范化后的页列表,恒非空。 */
150
+ pages: NonEmptyArray<ReportMetaPage>;
142
151
  }
143
152
 
144
153
  /** 单页缩写展开出的唯一页 id 与内置页名。 */
@@ -332,6 +341,53 @@ function assertHeadTags(tags: unknown): HeadTag[] {
332
341
  return tags as HeadTag[];
333
342
  }
334
343
 
344
+ /**
345
+ * page 的 input / navigation 规范化(shell.md「page 显式声明输入」):省略或 "scope" 时补
346
+ * `input: "scope"`,`navigation` 缺省为 true;"attempt" 必须显式 `navigation: false`——
347
+ * 没有 locator 时不可打开,不能悄悄挤进导航,省略或传 true 都在装载期报错。
348
+ */
349
+ function normalizePageInputAndNavigation(page: Record<string, unknown>): ReportPage {
350
+ const input = page.input;
351
+ if (input === undefined || input === "scope") {
352
+ return {
353
+ id: page.id as string,
354
+ title: page.title as LocalizedText,
355
+ content: page.content as ReportNode,
356
+ input: "scope",
357
+ navigation: page.navigation !== false,
358
+ };
359
+ }
360
+ if (input === "attempt") {
361
+ if (page.navigation !== false) {
362
+ throw new Error(
363
+ `Report page "${page.id}" declares input: "attempt" but not navigation: false — an attempt-input page has no content without a locator, so it must not appear in navigation. Add navigation: false.`,
364
+ );
365
+ }
366
+ return {
367
+ id: page.id as string,
368
+ title: page.title as LocalizedText,
369
+ content: page.content as ReportNode,
370
+ input: "attempt",
371
+ navigation: false,
372
+ };
373
+ }
374
+ throw new Error(
375
+ `Report page "${page.id}" input ${JSON.stringify(input)} is not valid — input is omitted, "scope", or "attempt".`,
376
+ );
377
+ }
378
+
379
+ /** 一份报告至多一张 attempt-input page,避免 show @<locator> 与 locator 链接出现多个目标。 */
380
+ function assertAtMostOneAttemptPage(pages: readonly ReportPage[]): void {
381
+ const attemptPages = pages.filter((p) => p.input === "attempt");
382
+ if (attemptPages.length > 1) {
383
+ throw new Error(
384
+ `A report can declare at most one input: "attempt" page — got ${attemptPages.length} (${attemptPages
385
+ .map((p) => `"${p.id}"`)
386
+ .join(", ")}). Keep one and remove the others, or fold their content into a single attempt-input page.`,
387
+ );
388
+ }
389
+ }
390
+
335
391
  export function defineReport(content: ReportNode): ReportDefinition;
336
392
  export function defineReport(def: ReportDef): ReportDefinition;
337
393
  export function defineReport(input: ReportNode | ReportDef): ReportDefinition {
@@ -379,7 +435,9 @@ export function defineReport(input: ReportNode | ReportDef): ReportDefinition {
379
435
  pages = base.pages;
380
436
  } else if (hasContent) {
381
437
  assertNotDefinition(def.content, 'defineReport "content"');
382
- pages = [{ id: DEFAULT_PAGE_ID, title: DEFAULT_PAGE_TITLE, content: def.content as ReportNode }];
438
+ pages = [
439
+ { id: DEFAULT_PAGE_ID, title: DEFAULT_PAGE_TITLE, input: "scope", navigation: true, content: def.content as ReportNode },
440
+ ];
383
441
  } else {
384
442
  const raw = def.pages as unknown;
385
443
  if (!Array.isArray(raw) || raw.length === 0) {
@@ -388,6 +446,7 @@ export function defineReport(input: ReportNode | ReportDef): ReportDefinition {
388
446
  );
389
447
  }
390
448
  const seen = new Set<string>();
449
+ const normalized: ReportPage[] = [];
391
450
  for (const page of raw as Array<Record<string, unknown>>) {
392
451
  if (typeof page?.id !== "string" || !PAGE_ID_PATTERN.test(page.id)) {
393
452
  throw new Error(
@@ -402,9 +461,11 @@ export function defineReport(input: ReportNode | ReportDef): ReportDefinition {
402
461
  seen.add(page.id);
403
462
  assertLocalizedText(page.title, `Report page "${page.id}" title`);
404
463
  assertNotDefinition(page.content, `Report page "${page.id}" content`);
464
+ normalized.push(normalizePageInputAndNavigation(page));
405
465
  }
406
- pages = raw as ReportPage[];
466
+ pages = normalized;
407
467
  }
468
+ assertAtMostOneAttemptPage(pages);
408
469
 
409
470
  if (def.title !== undefined) assertLocalizedText(def.title, "defineReport title");
410
471
  if (def.footer !== undefined) assertLocalizedText(def.footer, "defineReport footer");
@@ -482,145 +543,17 @@ export function resolveReportTitle(definition: ReportDefinition, scope: Scope):
482
543
  return names.every((name) => localizedTextEquals(name, first)) ? first : FALLBACK_REPORT_TITLE;
483
544
  }
484
545
 
485
- /** 规范化声明 → 组合组件可见的 ReportMeta(scripts / styles 是注入资产,不进)。 */
486
- export function buildReportMeta(definition: ReportDefinition, scope: Scope, pageId: string): ReportMeta {
546
+ /** 规范化声明 → 组合组件可见的 ReportMeta(scripts / styles 是注入资产,不进;不携带当前页)。 */
547
+ export function buildReportMeta(definition: ReportDefinition, scope: Scope): ReportMeta {
487
548
  return {
488
549
  title: resolveReportTitle(definition, scope),
489
550
  links: definition.links,
490
551
  ...(definition.footer !== undefined ? { footer: definition.footer } : {}),
491
- pages: definition.pages.map((page) => ({ id: page.id, title: page.title })) as unknown as NonEmptyArray<{
492
- id: string;
493
- title: LocalizedText;
494
- }>,
495
- pageId,
496
- };
497
- }
498
-
499
- // ───────────────────────── 页选择与 text 宿主入口 ─────────────────────────
500
-
501
- /** `--page` 未命中:宿主据此按用法错误退出并列出可用页 id。 */
502
- export class ReportPageNotFoundError extends Error {
503
- readonly pageId: string;
504
- readonly available: string[];
505
- constructor(pageId: string, available: string[]) {
506
- super(`page "${pageId}" not found. Available pages: ${available.join(", ")}`);
507
- this.pageId = pageId;
508
- this.available = available;
509
- }
510
- }
511
-
512
- export function pickReportPage(definition: ReportDefinition, pageId?: string): ReportPage {
513
- if (pageId === undefined) return definition.pages[0];
514
- const page = definition.pages.find((p) => p.id === pageId);
515
- if (!page) {
516
- throw new ReportPageNotFoundError(
517
- pageId,
518
- definition.pages.map((p) => p.id),
519
- );
520
- }
521
- return page;
522
- }
523
-
524
- /** 宿主注入的渲染上下文:官方口径挑好的 Scope 与结果根完整读取面。 */
525
- export interface ReportHostContext {
526
- scope: Scope;
527
- /** 组合组件 ctx.results 的来源;历史视图从这里自行挑 Snapshot[]。 */
528
- results: Results;
529
- }
530
-
531
- export interface RenderReportTextOptions extends TextRenderOptions {
532
- /** 渲染哪一页;缺省第一页。未命中抛 ReportPageNotFoundError。 */
533
- pageId?: string;
534
- }
535
-
536
- /**
537
- * text 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→ 树校验 →
538
- * 遍历渲染 text 面。不需要 react-dom。宿主不在报告树外另设警告通道——挑选警告的呈现件是
539
- * `ScopeWarnings` 组件,内建报告每页都放它,自定义报告放不放是作者义务
540
- * (docs/feature/reports/architecture.md「Scope 是计算入口」)。
541
- */
542
- export async function renderReportToText(
543
- definition: ReportDefinition,
544
- ctx: ReportHostContext,
545
- options?: RenderReportTextOptions,
546
- ): Promise<string> {
547
- const page = pickReportPage(definition, options?.pageId);
548
- const meta = buildReportMeta(definition, ctx.scope, page.id);
549
- const resolved = await resolveReportTree(page.content, {
550
- scope: ctx.scope,
551
- results: ctx.results,
552
- report: meta,
553
- memo: new ResolveMemo(),
554
- });
555
- validateReportTree(resolved);
556
- return renderNodeToText(resolved, createTextContext(options));
557
- }
558
-
559
- /** 页索引标题行(show 多页索引 / view 导航共用的解析结果):按 locale 解析的标题字符串。 */
560
- export function reportTitleText(definition: ReportDefinition, scope: Scope, locale: ReportLocale): string {
561
- return resolveLocalizedText(resolveReportTitle(definition, scope), locale);
562
- }
563
-
564
- // ───────────────────────── 逐页(树)渲染入口:宿主联系面 ─────────────────────────
565
-
566
- /** 宿主索引命令的完整上下文(docs/feature/reports/show/reports.md「索引命令携带完整上下文」)。 */
567
- export interface HostCommandContext {
568
- patterns: string[];
569
- results?: string;
570
- experiment?: string;
571
- report?: string;
572
- page?: string;
573
- }
574
-
575
- function quoteArg(value: string): string {
576
- return /^[A-Za-z0-9._/@-]+$/.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
577
- }
578
-
579
- /** 按上下文拼组索引的可复制命令:`niceeval show <patterns> --exp <id> [--results/--report/--page]`。 */
580
- function experimentCommandFor(ctx: HostCommandContext): (experimentIdPrefix: string) => string {
581
- return (prefix) => {
582
- const parts = ["niceeval show", ...ctx.patterns.map(quoteArg), `--exp ${quoteArg(prefix)}`];
583
- if (ctx.results !== undefined) parts.push(`--results ${quoteArg(ctx.results)}`);
584
- if (ctx.report !== undefined) parts.push(`--report ${quoteArg(ctx.report)}`);
585
- if (ctx.page !== undefined) parts.push(`--page ${quoteArg(ctx.page)}`);
586
- return parts.join(" ");
552
+ pages: definition.pages.map((page) => ({
553
+ id: page.id,
554
+ title: page.title,
555
+ input: page.input ?? "scope",
556
+ navigation: page.navigation ?? true,
557
+ })) as unknown as NonEmptyArray<ReportMetaPage>,
587
558
  };
588
559
  }
589
-
590
- /** 逐页渲染的宿主上下文:官方口径的 Scope、结果根读取面与规范化声明(ctx.report)。 */
591
- export interface ReportTreeHostContext {
592
- scope: Scope;
593
- results: Results;
594
- report: ReportMeta;
595
- }
596
-
597
- export interface RenderTreeTextOptions extends TextRenderOptions {
598
- /** 组索引命令的完整上下文;给了就按它拼命令,experimentCommand 显式注入时以后者为准。 */
599
- commandContext?: HostCommandContext;
600
- }
601
-
602
- /**
603
- * 渲染一页报告树的 text 面(宿主逐页调用;页选择归宿主):
604
- * resolve(组合展开 + spec 取数)→ validate → render。宿主不在报告树外另设警告通道,
605
- * 挑选警告由页内的 `ScopeWarnings` 组件呈现(内建报告每页都放它)。
606
- */
607
- export async function renderReportTreeToText(
608
- tree: ReportNode,
609
- ctx: ReportTreeHostContext,
610
- options?: RenderTreeTextOptions,
611
- ): Promise<string> {
612
- const resolved = await resolveReportTree(tree, {
613
- scope: ctx.scope,
614
- results: ctx.results,
615
- report: ctx.report,
616
- memo: new ResolveMemo(),
617
- });
618
- validateReportTree(resolved);
619
- const textCtx = createTextContext({
620
- ...options,
621
- ...(options?.experimentCommand === undefined && options?.commandContext !== undefined
622
- ? { experimentCommand: experimentCommandFor(options.commandContext) }
623
- : {}),
624
- });
625
- return renderNodeToText(resolved, textCtx);
626
- }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, expect, it } from "vitest";
7
7
 
8
- import type { Scope } from "../results/index.ts";
8
+ import type { Scope } from "../../results/index.ts";
9
9
  import { buildReportMeta, defineReport } from "./report.ts";
10
10
 
11
11
  const emptyScope = { snapshots: [] } as unknown as Scope;
@@ -97,6 +97,6 @@ describe("defineReport head 通道(装载校验)", () => {
97
97
  ];
98
98
  const definition = defineReport({ content: null, head });
99
99
  expect(definition.head).toEqual(head);
100
- expect(buildReportMeta(definition, emptyScope, "report")).not.toHaveProperty("head");
100
+ expect(buildReportMeta(definition, emptyScope)).not.toHaveProperty("head");
101
101
  });
102
102
  });
@@ -6,10 +6,10 @@
6
6
  // 列宽按显示宽度算(CJK 记 2 列);null 渲染 —,不补 0;超宽先折最宽的左对齐列,
7
7
  // 压到下限仍放不下就从右侧丢列并如实报数(「截断报剩余」是既有契约,不在这里破例)。
8
8
 
9
- import type { TableProps, TableRow } from "../primitives.tsx";
10
- import type { TextContext } from "../tree.ts";
11
- import { countText, localeText, resolveLocalizedText } from "../locale.ts";
12
- import { charDisplayWidth, renderAlignedRows, stringWidth, wrapDisplay, type ColumnAlign } from "./layout.ts";
9
+ import type { TableProps, TableRow } from "./primitives.tsx";
10
+ import type { TextContext } from "./tree.ts";
11
+ import { countText, localeText, resolveLocalizedText } from "../model/locale.ts";
12
+ import { charDisplayWidth, renderAlignedRows, stringWidth, wrapDisplay, type ColumnAlign } from "../model/text-layout.ts";
13
13
 
14
14
  const MISSING_MARK = "—";
15
15
  /** 列间距,与 renderAlignedRows 的 join(" ") 一致。 */