niceeval 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -1,61 +1,79 @@
1
1
  // 官方双面组件的装配点:web 面(./react/ 的纯 React 组件)+ text 面(./text/faces.ts)
2
- // + 挂在组件上的 data 计算函数。faces 两键必填 —— 配对是结构义务,不是配对表;
3
- // MetricBars.data 就是 MetricMatrix.data(同一份矩阵数据的另一种摆法),别名显式化。
2
+ // + resolve 解析面(spec 形态由管线代调配套 *Data 函数)。faces 两键必填 —— 配对是结构
3
+ // 义务;MetricBars MetricMatrix 消费同一份矩阵数据(同一个 metricMatrixData)
4
+ //
5
+ // props 双形态以 `data` 判别(docs/feature/reports/library/metric-views.md):
6
+ // spec 形态 = 计算选项平铺 + 可选 `input`(默认宿主注入的 Scope),管线在 resolve 阶段
7
+ // 代调同名 *Data,与手工计算严格等价;data 形态接收算好的可序列化数据,跳过取数。
8
+ // 同一组件同时给出 `data` 与 spec 字段按完整用户反馈报错,不静默取一边。
9
+ // 组件消费 `data` 时校验结构,不符合当前形状按完整用户反馈报错并提示可能的版本漂移。
4
10
  //
5
11
  // 官方组件在宿主里自动接上证据室:web 面的 attemptHref 缺省取 ctx.attemptHref
6
- // (宿主注入的证据室深链);显式传 prop 可覆盖(嵌进自己应用时自定去处)。三个实体列表
7
- // (ExperimentList / EvalList / AttemptList)没有这个覆盖口子——它们的 locator 徽标恒经
8
- // ctx.attemptHref / ctx.attemptCommand,不接受 attemptHref/attemptCommand prop(docs/feature/reports/library.md
9
- // 「嵌入自己的 React 页面」的函数签名没有这个参数),证据室深链在这三个组件上不是可选行为。
12
+ // (宿主注入的证据室深链);显式传 prop 可覆盖(嵌进自己应用时自定去处)
10
13
 
11
- import { defineComponent, isHostWebContextActive } from "./tree.ts";
12
- import type { ReportComponent } from "./tree.ts";
14
+ import type { ReactNode } from "react";
15
+ import {
16
+ defineComponent,
17
+ isHostWebContextActive,
18
+ memoFetchOf,
19
+ type ReportComponent,
20
+ type ResolveContext,
21
+ type TextContext,
22
+ type WebContext,
23
+ } from "./tree.ts";
13
24
  import type { ReportLocale } from "./locale.ts";
14
25
  import type { AttemptLocator } from "../results/locator.ts";
15
- import type { Selection } from "../results/types.ts";
16
26
  import type {
17
27
  AttemptListItem,
18
28
  DeltaData,
29
+ EntityListDataOptions,
19
30
  EvalListItem,
31
+ ExperimentComparisonData,
20
32
  ExperimentListItem,
21
- GroupSummaryData,
22
33
  LineData,
23
34
  MatrixData,
24
- OverviewData,
35
+ ReportInput,
25
36
  ScatterData,
37
+ ScopeSummaryData,
26
38
  ScoreboardData,
27
39
  TableData,
28
40
  } from "./types.ts";
29
41
  import {
30
42
  attemptListData,
31
- deltaData,
43
+ deltaTableData,
32
44
  evalListData,
45
+ experimentComparisonData,
33
46
  experimentListData,
34
- groupSummaryData,
35
- lineData,
36
- matrixData,
37
- overviewData,
38
- scatterData,
47
+ metricLineData,
48
+ metricMatrixData,
49
+ metricScatterData,
50
+ metricTableData,
51
+ scopeSummaryData,
39
52
  scoreboardData,
40
- tableData,
53
+ type DeltaTableOptions,
54
+ type MetricLineOptions,
55
+ type MetricMatrixOptions,
56
+ type MetricScatterOptions,
57
+ type MetricTableOptions,
58
+ type ScoreboardOptions,
41
59
  } from "./compute.ts";
42
- import type { ScatterDataOptions } from "./compute.ts";
60
+ import { collectItems, locatorOf, resolveInput } from "./aggregate.ts";
43
61
  import {
44
62
  attemptListText,
45
- barsText,
46
63
  deltaText,
47
64
  evalListText,
65
+ experimentComparisonText,
48
66
  experimentListText,
49
- groupSummaryText,
67
+ barsText,
50
68
  lineText,
51
69
  matrixText,
52
- overviewText,
53
70
  scatterText,
54
71
  scoreboardText,
72
+ scopeSummaryText,
55
73
  tableText,
56
74
  } from "./text/faces.ts";
57
- import { RunOverview as RunOverviewWeb } from "./react/RunOverview.tsx";
58
- import { GroupSummary as GroupSummaryWeb } from "./react/GroupSummary.tsx";
75
+ import { ScopeSummary as ScopeSummaryWeb } from "./react/ScopeSummary.tsx";
76
+ import { ExperimentComparisonView } from "./react/ExperimentComparison.tsx";
59
77
  import { ExperimentList as ExperimentListWeb } from "./react/ExperimentList.tsx";
60
78
  import { EvalList as EvalListWeb } from "./react/EvalList.tsx";
61
79
  import { AttemptList as AttemptListWeb } from "./react/AttemptList.tsx";
@@ -67,299 +85,627 @@ import { MetricScatter as MetricScatterWeb } from "./react/MetricScatter.tsx";
67
85
  import { MetricLine as MetricLineWeb } from "./react/MetricLine.tsx";
68
86
  import { DeltaTable as DeltaTableWeb } from "./react/DeltaTable.tsx";
69
87
 
70
- // ───────────────────────── props ─────────────────────────
88
+ // ───────────────────────── DataProps 组合规则 ─────────────────────────
71
89
 
72
- export interface RunOverviewProps {
73
- data: OverviewData;
74
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
75
- locale?: ReportLocale;
76
- className?: string;
77
- }
78
-
79
- export interface GroupSummaryProps {
80
- data: GroupSummaryData;
81
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
82
- locale?: ReportLocale;
83
- className?: string;
84
- }
90
+ type Never<T> = { [K in keyof T]?: never };
85
91
 
86
92
  /**
87
- * 三个实体列表都只收算好的 `items`(`.data(selection)` 的产物,报告作者先 `.filter()`/
88
- * `.slice()` 再传入)——没有 selection-form,组件不提供另一套过滤 DSL
89
- * (docs/feature/reports/library.md「实体列表」)。
93
+ * 官方数据组件的统一 props 组合(docs/feature/reports/library/metric-views.md):
94
+ * data 形态(接收配套 *Data 的产物) spec 形态(Options 平铺 + 可选 input)。
90
95
  */
91
- export interface ExperimentListProps {
92
- items: ExperimentListItem[];
93
- /** web 面在比较表前显示实验过滤框;text 面忽略。 */
94
- filter?: boolean;
95
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
96
- locale?: ReportLocale;
97
- className?: string;
98
- }
96
+ export type DataProps<Data, Options, Presentation> =
97
+ | ({ data: Data; input?: never } & Never<Options> & Presentation)
98
+ | ({ data?: never; input?: ReportInput } & Options & Presentation);
99
99
 
100
- export interface EvalListProps {
101
- items: EvalListItem[];
102
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
103
- locale?: ReportLocale;
104
- className?: string;
105
- }
100
+ // ───────────────────────── data 结构校验(版本漂移防线)─────────────────────────
106
101
 
107
- export interface AttemptListProps {
108
- items: AttemptListItem[];
109
- /** items 被 slice 时如实显示剩余数量;省略即 items.length(未截断)。 */
110
- total?: number;
111
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
112
- locale?: ReportLocale;
113
- className?: string;
102
+ function isObject(value: unknown): value is Record<string, unknown> {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
114
104
  }
115
105
 
116
- export interface MetricTableProps {
117
- data: TableData;
118
- /** 传了,格子可点、下钻去处你定;不传,宿主里走证据室深链,宿主外纯展示。 */
119
- attemptHref?: (locator: AttemptLocator) => string;
120
- /**
121
- * web 面在表格前渲染一个过滤输入框(`<input class="nre-filter" data-nre-filter>`),
122
- * 由渐进增强 runtime(enhance.js)接管:输入过滤行 textContent。无 JS 时输入框
123
- * 静默无功能,表格内容依旧完整可读。默认 false;text 面不受影响。
124
- */
125
- filter?: boolean;
126
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
127
- locale?: ReportLocale;
128
- className?: string;
106
+ function isCell(value: unknown): boolean {
107
+ return (
108
+ isObject(value) &&
109
+ "value" in value &&
110
+ "display" in value &&
111
+ typeof value.samples === "number" &&
112
+ typeof value.total === "number" &&
113
+ Array.isArray(value.refs)
114
+ );
129
115
  }
130
116
 
131
- export interface MetricMatrixProps {
132
- data: MatrixData;
133
- attemptHref?: (locator: AttemptLocator) => string;
134
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
135
- locale?: ReportLocale;
136
- className?: string;
117
+ function isTally(value: unknown): boolean {
118
+ return (
119
+ isObject(value) &&
120
+ typeof value.passed === "number" &&
121
+ typeof value.failed === "number" &&
122
+ typeof value.errored === "number" &&
123
+ typeof value.skipped === "number"
124
+ );
137
125
  }
138
126
 
139
- export interface ScoreboardProps {
140
- data: ScoreboardData;
141
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en") */
142
- locale?: ReportLocale;
143
- className?: string;
127
+ type Validator = (data: unknown) => string | null;
128
+
129
+ function dataShapeError(component: string, dataFnName: string, shape: string, problem: string): Error {
130
+ return new Error(
131
+ `<${component}> received data that does not match the current ${shape} shape: ${problem}. ` +
132
+ `It may have been computed by a different niceeval version (component data carries no schemaVersion; the support window is same-version write and read). ` +
133
+ `Recompute it with ${dataFnName}() from this niceeval version, then re-render.`,
134
+ );
144
135
  }
145
136
 
146
- /** MetricScatter 两臂共享的纯渲染选项(不含数据来源) */
147
- interface MetricScatterRenderProps {
148
- /** 点一个点 该配置的下钻页。 */
149
- pointHref?: (row: ScatterData["rows"][number]) => string;
150
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
151
- locale?: ReportLocale;
152
- className?: string;
137
+ const validateTableData: Validator = (data) => {
138
+ if (!isObject(data)) return "expected an object";
139
+ if (typeof data.rowDimension !== "string") return 'missing "rowDimension" (string)';
140
+ if (!Array.isArray(data.columns)) return 'missing "columns" (array)';
141
+ if (!Array.isArray(data.rows)) return 'missing "rows" (array)';
142
+ for (const row of data.rows as unknown[]) {
143
+ if (!isObject(row) || typeof row.key !== "string" || !isObject(row.cells)) {
144
+ return 'each row needs { key, cells }';
145
+ }
146
+ }
147
+ return null;
148
+ };
149
+
150
+ const validateMatrixData: Validator = (data) => {
151
+ if (!isObject(data)) return "expected an object";
152
+ if (typeof data.rowDimension !== "string" || typeof data.columnDimension !== "string") {
153
+ return 'missing "rowDimension" / "columnDimension" (string)';
154
+ }
155
+ if (!isObject(data.metric)) return 'missing "metric" (MetricColumn)';
156
+ if (!Array.isArray(data.cells)) return 'missing "cells" (array)';
157
+ return null;
158
+ };
159
+
160
+ const validateScatterData: Validator = (data) => {
161
+ if (!isObject(data)) return "expected an object";
162
+ if (typeof data.pointDimension !== "string") return 'missing "pointDimension" (string)';
163
+ if (!isObject(data.x) || !isObject(data.y)) return 'missing "x" / "y" (MetricColumn)';
164
+ if (!Array.isArray(data.rows)) return 'missing "rows" (array)';
165
+ for (const row of data.rows as unknown[]) {
166
+ if (!isObject(row) || typeof row.key !== "string" || !isCell(row.x) || !isCell(row.y)) {
167
+ return "each row needs { key, x: MetricCell, y: MetricCell }";
168
+ }
169
+ }
170
+ return null;
171
+ };
172
+
173
+ const validateLineData: Validator = (data) => {
174
+ if (!isObject(data)) return "expected an object";
175
+ if (!isObject(data.x) || typeof (data.x as Record<string, unknown>).key !== "string") return 'missing "x" axis descriptor';
176
+ if (!isObject(data.y)) return 'missing "y" (MetricColumn)';
177
+ if (!Array.isArray(data.rows)) return 'missing "rows" (array)';
178
+ return null;
179
+ };
180
+
181
+ const validateScoreboardData: Validator = (data) => {
182
+ if (!isObject(data)) return "expected an object";
183
+ if (typeof data.rowDimension !== "string") return 'missing "rowDimension" (string)';
184
+ if (!Array.isArray(data.questions)) return 'missing "questions" (array)';
185
+ if (typeof data.fullMarks !== "number") return 'missing "fullMarks" (number)';
186
+ if (typeof data.ignoredEvals !== "number") return 'missing "ignoredEvals" (number)';
187
+ if (!Array.isArray(data.rows)) return 'missing "rows" (array)';
188
+ for (const row of data.rows as unknown[]) {
189
+ if (!isObject(row) || !isObject(row.total) || typeof (row.total as Record<string, unknown>).notRun !== "number") {
190
+ return 'each row needs { key, total: { value, display, notRun, unscorable, refs }, subjects }';
191
+ }
192
+ }
193
+ return null;
194
+ };
195
+
196
+ const validateDeltaData: Validator = (data) => {
197
+ if (!isObject(data)) return "expected an object";
198
+ if (typeof data.byDimension !== "string") return 'missing "byDimension" (string)';
199
+ if (!Array.isArray(data.columns) || !Array.isArray(data.rows)) return 'missing "columns" / "rows" (array)';
200
+ for (const row of data.rows as unknown[]) {
201
+ if (!isObject(row) || row.label === undefined || !isObject(row.a) || !isObject(row.b)) {
202
+ return "each row needs { key, label, a, b, cells }";
203
+ }
204
+ }
205
+ return null;
206
+ };
207
+
208
+ const validateScopeSummaryData: Validator = (data) => {
209
+ if (!isObject(data)) return "expected an object";
210
+ if (!isObject(data.range)) return 'missing "range" ({ earliestStartedAt, latestStartedAt })';
211
+ if (!isTally(data.evalVerdicts) || !isTally(data.attemptVerdicts)) {
212
+ return 'missing "evalVerdicts" / "attemptVerdicts" tallies';
213
+ }
214
+ if (!isCell(data.endToEndPassRate) || !isCell(data.totalCostUSD)) {
215
+ return 'missing "endToEndPassRate" / "totalCostUSD" (MetricCell)';
216
+ }
217
+ return null;
218
+ };
219
+
220
+ const validateComparisonData: Validator = (data) => {
221
+ if (!isObject(data)) return "expected an object";
222
+ if (!Array.isArray(data.groups)) return 'missing "groups" (array)';
223
+ for (const group of data.groups as unknown[]) {
224
+ if (!isObject(group) || typeof group.key !== "string" || validateScopeSummaryData(group.summary) !== null) {
225
+ return "each group needs { key, summary, scatter, experiments }";
226
+ }
227
+ }
228
+ return null;
229
+ };
230
+
231
+ const validateExperimentListData: Validator = (data) => {
232
+ if (!Array.isArray(data)) return "expected an array of ExperimentListItem";
233
+ for (const item of data as unknown[]) {
234
+ if (!isObject(item) || typeof item.experimentId !== "string" || !isTally(item.evalVerdicts) || !isCell(item.endToEndPassRate)) {
235
+ return "each item needs { experimentId, evalVerdicts, endToEndPassRate, costUSD, durationMs, tokens, evalRows, … }";
236
+ }
237
+ }
238
+ return null;
239
+ };
240
+
241
+ const validateEvalListData: Validator = (data) => {
242
+ if (!Array.isArray(data)) return "expected an array of EvalListItem";
243
+ for (const item of data as unknown[]) {
244
+ if (!isObject(item) || typeof item.evalId !== "string" || !isCell(item.examScore) || !Array.isArray(item.attempts)) {
245
+ return "each item needs { experimentId, evalId, verdict, examScore, durationMs, costUSD, attempts }";
246
+ }
247
+ }
248
+ return null;
249
+ };
250
+
251
+ const validateAttemptListData: Validator = (data) => {
252
+ if (!Array.isArray(data)) return "expected an array of AttemptListItem";
253
+ for (const item of data as unknown[]) {
254
+ if (!isObject(item) || typeof item.evalId !== "string" || !("failureSummary" in item) || !("costUSD" in item)) {
255
+ return "each item needs { experimentId, evalId, verdict, failureSummary, moreFailures, examScore, durationMs, costUSD, locator }";
256
+ }
257
+ }
258
+ return null;
259
+ };
260
+
261
+ // ───────────────────────── spec / data 双形态的通用装配 ─────────────────────────
262
+
263
+ interface DataComponentDef<Data, Options, Presentation> {
264
+ name: string;
265
+ dataFnName: string;
266
+ shapeName: string;
267
+ dataFn: (input: ReportInput, options: Options) => Promise<Data>;
268
+ /** spec 形态的计算选项 prop 名(不含 input);未列出的 props 视为呈现选项原样保留。 */
269
+ specKeys: readonly string[];
270
+ validate: Validator;
271
+ web(props: { data: Data } & Presentation, ctx: WebContext): ReactNode;
272
+ text(props: { data: Data } & Presentation, ctx: TextContext): string;
153
273
  }
154
274
 
155
- /** resolve 之后 web / text 面看到的形态:数据已备好,零 IO。 */
156
- export interface MetricScatterResolvedProps extends MetricScatterRenderProps {
157
- data: ScatterData;
275
+ function makeDataComponent<Data, Options, Presentation>(
276
+ def: DataComponentDef<Data, Options, Presentation>,
277
+ ): ReportComponent<DataProps<Data, Options, Presentation>> {
278
+ type Props = Record<string, unknown>;
279
+ type Resolved = { data: Data } & Presentation;
280
+
281
+ const assertData = (data: unknown): Data => {
282
+ const problem = def.validate(data);
283
+ if (problem !== null) throw dataShapeError(def.name, def.dataFnName, def.shapeName, problem);
284
+ return data as Data;
285
+ };
286
+
287
+ const resolve = async (props: Props, ctx: ResolveContext): Promise<Resolved> => {
288
+ const givenSpec = def.specKeys.filter((key) => props[key] !== undefined);
289
+ if (props.data !== undefined) {
290
+ if (givenSpec.length > 0 || props.input !== undefined) {
291
+ const extras = [...givenSpec, ...(props.input !== undefined ? ["input"] : [])];
292
+ throw new Error(
293
+ `<${def.name}> got both \`data\` and spec field${extras.length > 1 ? "s" : ""} (${extras.join(", ")}) — the two data sources are exclusive and niceeval will not silently pick one. ` +
294
+ `Keep \`data\` (precomputed with ${def.dataFnName}()) and drop the spec fields, or drop \`data\` and let the pipeline compute from the spec.`,
295
+ );
296
+ }
297
+ assertData(props.data);
298
+ return props as unknown as Resolved;
299
+ }
300
+ const options: Record<string, unknown> = {};
301
+ for (const key of givenSpec) options[key] = props[key];
302
+ const input = (props.input as ReportInput | undefined) ?? ctx.input;
303
+ const data = await memoFetchOf(ctx)(def.dataFn, input, options, () =>
304
+ def.dataFn(input, options as Options),
305
+ );
306
+ const rest: Record<string, unknown> = { ...props };
307
+ delete rest.input;
308
+ for (const key of def.specKeys) delete rest[key];
309
+ return { ...rest, data } as unknown as Resolved;
310
+ };
311
+
312
+ const component = defineComponent<Props, Resolved>({
313
+ resolve,
314
+ web: (props, ctx) => {
315
+ assertData((props as { data?: unknown }).data);
316
+ return def.web(props, ctx);
317
+ },
318
+ text: (props, ctx) => {
319
+ assertData((props as { data?: unknown }).data);
320
+ return def.text(props, ctx);
321
+ },
322
+ }) as unknown as ReportComponent<DataProps<Data, Options, Presentation>>;
323
+ component.displayName = def.name;
324
+ return component;
158
325
  }
159
326
 
160
- /**
161
- * 互斥两臂:要么直接给算好的 `data`(用于用户自己的 React 页面 / 预计算 / 跨边界序列化),
162
- * 要么给 `selection` + 计算选项让宿主在渲染前解析(见 tree.ts 的 resolveReportTree)。
163
- * 同时传 `data` 与 `selection`、或两者都不传,都在 typecheck 阶段失败。计算选项复用
164
- * `ScatterDataOptions`,不手写会漂移的副本。
165
- */
166
- export type MetricScatterProps =
167
- | (MetricScatterResolvedProps & {
168
- selection?: never;
169
- points?: never;
170
- series?: never;
171
- x?: never;
172
- y?: never;
173
- })
174
- | ({ data?: never } & ScatterDataOptions & MetricScatterRenderProps & { selection: Selection });
175
-
176
- export interface MetricLineProps {
177
- data: LineData;
178
- pointHref?: (row: LineData["rows"][number]) => string;
179
- /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
180
- locale?: ReportLocale;
181
- className?: string;
327
+ /** 宿主内缺省接证据室,显式 prop 覆盖;宿主外不传 attemptHref 就是纯展示。 */
328
+ function hrefOf(
329
+ props: { attemptHref?: (locator: AttemptLocator) => string },
330
+ ctx: WebContext,
331
+ ): ((locator: AttemptLocator) => string) | undefined {
332
+ return props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined);
182
333
  }
183
334
 
184
- export interface DeltaTableProps {
185
- data: DeltaData;
335
+ // ───────────────────────── 呈现选项类型 ─────────────────────────
336
+
337
+ interface ChromeProps {
186
338
  /** chrome 文案 locale;省略时随宿主上下文(宿主外默认 "en")。 */
187
339
  locale?: ReportLocale;
188
340
  className?: string;
189
341
  }
190
342
 
191
- // ───────────────────────── 装配 ─────────────────────────
343
+ // ───────────────────────── 概览组件 ─────────────────────────
192
344
 
193
- const SCATTER_UNRESOLVED_MESSAGE =
194
- "MetricScatter received unresolved (selection-form) props outside the report host pipeline. " +
195
- "Render through defineReport + niceeval show/view (or renderReportToText/renderReportToStaticHtml), " +
196
- "or precompute with `await MetricScatter.data(selection, options)` and pass the result as `data`.";
345
+ export type ScopeSummaryProps = DataProps<
346
+ ScopeSummaryData,
347
+ Record<never, never>,
348
+ ChromeProps & {
349
+ /** 显示哪一级计票;默认 "eval"。data 恒携带两级,votes 只选择呈现。 */
350
+ votes?: "eval" | "attempt";
351
+ }
352
+ >;
197
353
 
198
- /** 渲染面(web / text)只吃已解析的数据形态;selection 形态漏解析时直说,而不是画一张空组件。 */
199
- function requireScatterData(props: { data?: ScatterData }): ScatterData {
200
- if (props.data === undefined) throw new Error(SCATTER_UNRESOLVED_MESSAGE);
201
- return props.data;
202
- }
354
+ /** 范围摘要卡:时间窗、数量、两级计票、端到端成功率与总成本。 */
355
+ export const ScopeSummary = makeDataComponent<
356
+ ScopeSummaryData,
357
+ Record<never, never>,
358
+ ChromeProps & { votes?: "eval" | "attempt" }
359
+ >({
360
+ name: "ScopeSummary",
361
+ dataFnName: "scopeSummaryData",
362
+ shapeName: "ScopeSummaryData",
363
+ dataFn: (input) => scopeSummaryData(input),
364
+ specKeys: [],
365
+ validate: validateScopeSummaryData,
366
+ web: (props, ctx) => <ScopeSummaryWeb {...props} locale={props.locale ?? ctx.locale} />,
367
+ text: (props, ctx) => scopeSummaryText(props.data, props.votes ?? "eval", ctx),
368
+ }) as unknown as ReportComponent<ScopeSummaryProps>;
203
369
 
204
- /** 页头 KPI 条:何时跑的、几个配置、几道题、通过率、总成本;Selection 的警告随行显示在条内。 */
205
- export const RunOverview: ReportComponent<RunOverviewProps> & { data: typeof overviewData } = Object.assign(
206
- defineComponent<RunOverviewProps>({
207
- web: (props, ctx) => <RunOverviewWeb {...props} locale={props.locale ?? ctx.locale} />,
208
- text: ({ data }, ctx) => overviewText(data, ctx),
209
- }),
210
- { data: overviewData },
211
- );
212
- RunOverview.displayName = "RunOverview";
370
+ export type ExperimentComparisonProps = DataProps<ExperimentComparisonData, Record<never, never>, ChromeProps>;
213
371
 
214
372
  /**
215
- * 组摘要:一组 experiment(典型用法是同一 `<Section>` 内的全部 experiment)的紧凑统计——
216
- * 通过率(旧 GroupSelector 卡片口径)、experiment/eval/attempt 数、eval 级折叠计票、
217
- * 总成本、最后运行时间。
373
+ * 内建报告的默认组合件:先把 input 按可比组分区,再为每组分别计算 ScopeSummary、
374
+ * 成本 × 端到端成功率散点和 ExperimentList。web 面持有完整组索引并一次聚焦一组;
375
+ * text 面命中多个组时只显示组索引与可执行的单组查看命令,命中单组时才输出完整散点与列表。
218
376
  */
219
- export const GroupSummary: ReportComponent<GroupSummaryProps> & { data: typeof groupSummaryData } = Object.assign(
220
- defineComponent<GroupSummaryProps>({
221
- web: (props, ctx) => <GroupSummaryWeb {...props} locale={props.locale ?? ctx.locale} />,
222
- text: ({ data }, ctx) => groupSummaryText(data, ctx),
223
- }),
224
- { data: groupSummaryData },
225
- );
226
- GroupSummary.displayName = "GroupSummary";
377
+ export const ExperimentComparison = makeDataComponent<ExperimentComparisonData, Record<never, never>, ChromeProps>({
378
+ name: "ExperimentComparison",
379
+ dataFnName: "experimentComparisonData",
380
+ shapeName: "ExperimentComparisonData",
381
+ dataFn: (input) => experimentComparisonData(input),
382
+ specKeys: [],
383
+ validate: validateComparisonData,
384
+ web: (props, ctx) => (
385
+ <ExperimentComparisonView
386
+ data={props.data}
387
+ locale={props.locale ?? ctx.locale}
388
+ className={props.className}
389
+ attemptHref={isHostWebContextActive() ? ctx.attemptHref : undefined}
390
+ />
391
+ ),
392
+ text: (props, ctx) => experimentComparisonText(props.data, props.className, ctx),
393
+ }) as unknown as ReportComponent<ExperimentComparisonProps>;
394
+
395
+ // ───────────────────────── 实体列表 ─────────────────────────
396
+
397
+ interface EntityListChrome extends ChromeProps {
398
+ attemptHref?: (locator: AttemptLocator) => string;
399
+ }
400
+
401
+ export type ExperimentListProps = DataProps<
402
+ readonly ExperimentListItem[],
403
+ EntityListDataOptions,
404
+ EntityListChrome & {
405
+ /** web 面在比较表前显示实验过滤框;text 面忽略。 */
406
+ filter?: boolean;
407
+ /**
408
+ * 可选父路径:两面的行标签去掉与它相同的前缀,只显示 experiment id 末段(默认
409
+ * `ExperimentComparison` 给每组传组键)。完整 id 仍是排序 / 着色 / 过滤 / 折叠的键。
410
+ */
411
+ relativeTo?: string;
412
+ }
413
+ >;
414
+
415
+ /** 实验列表:每项一个 experiment,固定八列比较表 + 展开到 Eval / Attempt。 */
416
+ export const ExperimentList = makeDataComponent<
417
+ readonly ExperimentListItem[],
418
+ EntityListDataOptions,
419
+ EntityListChrome & { filter?: boolean; relativeTo?: string }
420
+ >({
421
+ name: "ExperimentList",
422
+ dataFnName: "experimentListData",
423
+ shapeName: "ExperimentListItem[]",
424
+ dataFn: (input, options) => experimentListData(input, options),
425
+ specKeys: ["redact"],
426
+ validate: validateExperimentListData,
427
+ web: (props, ctx) => (
428
+ <ExperimentListWeb
429
+ data={props.data}
430
+ filter={props.filter}
431
+ relativeTo={props.relativeTo}
432
+ locale={props.locale ?? ctx.locale}
433
+ attemptHref={hrefOf(props, ctx) ?? ctx.attemptHref}
434
+ className={props.className}
435
+ />
436
+ ),
437
+ text: (props, ctx) => experimentListText(props.data, ctx, props.relativeTo),
438
+ }) as unknown as ReportComponent<ExperimentListProps>;
439
+
440
+ export type EvalListProps = DataProps<readonly EvalListItem[], EntityListDataOptions, EntityListChrome>;
441
+
442
+ /** Eval 列表:每项一个 experimentId + evalId,展开到这道题的 Attempt。 */
443
+ export const EvalList = makeDataComponent<readonly EvalListItem[], EntityListDataOptions, EntityListChrome>({
444
+ name: "EvalList",
445
+ dataFnName: "evalListData",
446
+ shapeName: "EvalListItem[]",
447
+ dataFn: (input, options) => evalListData(input, options),
448
+ specKeys: ["redact"],
449
+ validate: validateEvalListData,
450
+ web: (props, ctx) => (
451
+ <EvalListWeb
452
+ data={props.data}
453
+ locale={props.locale ?? ctx.locale}
454
+ attemptHref={hrefOf(props, ctx) ?? ctx.attemptHref}
455
+ className={props.className}
456
+ />
457
+ ),
458
+ text: (props, ctx) => evalListText(props.data, ctx),
459
+ }) as unknown as ReportComponent<EvalListProps>;
460
+
461
+ export type AttemptListProps = DataProps<
462
+ readonly AttemptListItem[],
463
+ EntityListDataOptions,
464
+ EntityListChrome & {
465
+ /** 过滤 / 截断前的总数;省略时等于 data 长度。 */
466
+ total?: number;
467
+ }
468
+ >;
469
+
470
+ /** Attempt 列表:实体列表的叶子层,每项一次 attempt 的判定、单行摘要与 locator。 */
471
+ export const AttemptList = makeDataComponent<
472
+ readonly AttemptListItem[],
473
+ EntityListDataOptions,
474
+ EntityListChrome & { total?: number }
475
+ >({
476
+ name: "AttemptList",
477
+ dataFnName: "attemptListData",
478
+ shapeName: "AttemptListItem[]",
479
+ dataFn: (input, options) => attemptListData(input, options),
480
+ specKeys: ["redact"],
481
+ validate: validateAttemptListData,
482
+ web: (props, ctx) => (
483
+ <AttemptListWeb
484
+ data={props.data}
485
+ total={props.total}
486
+ locale={props.locale ?? ctx.locale}
487
+ attemptHref={hrefOf(props, ctx) ?? ctx.attemptHref}
488
+ className={props.className}
489
+ />
490
+ ),
491
+ text: (props, ctx) => attemptListText(props.data, props.total, ctx),
492
+ }) as unknown as ReportComponent<AttemptListProps>;
493
+
494
+ // ───────────────────────── FailureList(官方组合件)─────────────────────────
495
+
496
+ export interface FailureListProps {
497
+ /** 显示的最大条数;默认 20。 */
498
+ limit?: number;
499
+ /** 默认宿主注入的 Scope。 */
500
+ input?: ReportInput;
501
+ redact?: (text: string) => string;
502
+ attemptHref?: (locator: AttemptLocator) => string;
503
+ locale?: ReportLocale;
504
+ className?: string;
505
+ }
227
506
 
228
507
  /**
229
- * 实验列表:每项一个 experiment,固定展示身份、配置(flags)、Eval 判定构成和官方两级聚合
230
- * 汇总指标;展开到这个 experiment 的 Eval,每道题内联该题全部 Attempt 的 locator 徽标。
231
- * 没有 selection-form:`ExperimentList.data(selection)` 产出普通数组,过滤由报告作者对
232
- * 数组调用 `.filter()`。
508
+ * 「现在有哪些失败要处理」的成品组合件:内部就是 attemptListData → 过滤 → AttemptList
509
+ * data 形态,与手写组合严格等价、没有私有能力(docs/feature/reports/library/entity-lists.md)。
510
+ * verdict ∈ failed / errored,按 attempt 开始时间降序(同刻按 locator 字典序),
511
+ * 截断到 limit(默认 20),total 报告截断前总数。
233
512
  */
234
- export const ExperimentList: ReportComponent<ExperimentListProps> & { data: typeof experimentListData } =
235
- Object.assign(
236
- defineComponent<ExperimentListProps>({
237
- web: (props, ctx) => <ExperimentListWeb {...props} locale={props.locale ?? ctx.locale} attemptHref={ctx.attemptHref} />,
238
- text: ({ items }, ctx) => experimentListText(items, ctx),
239
- }),
240
- { data: experimentListData },
513
+ export const FailureList = defineComponent<FailureListProps>(async (props, ctx) => {
514
+ const input = props.input ?? ctx.scope;
515
+ const all = await attemptListData(input, props.redact !== undefined ? { redact: props.redact } : undefined);
516
+ // attempt 开始时间不在列表条目里(它不是列表展示字段);从同一 input 的读取面按 locator 对回。
517
+ const startedAtByLocator = new Map<string, string>();
518
+ for (const item of collectItems(resolveInput(input).snapshots)) {
519
+ startedAtByLocator.set(locatorOf(item), item.attempt.result.startedAt ?? "");
520
+ }
521
+ const failures = all
522
+ .filter((item) => item.verdict === "failed" || item.verdict === "errored")
523
+ .sort((a, b) => {
524
+ const ta = startedAtByLocator.get(a.locator) ?? "";
525
+ const tb = startedAtByLocator.get(b.locator) ?? "";
526
+ if (ta !== tb) return ta < tb ? 1 : -1; // 最近的失败在前
527
+ return a.locator < b.locator ? -1 : a.locator > b.locator ? 1 : 0;
528
+ });
529
+ const limit = props.limit ?? 20;
530
+ return (
531
+ <AttemptList
532
+ data={failures.slice(0, limit)}
533
+ total={failures.length}
534
+ attemptHref={props.attemptHref}
535
+ locale={props.locale}
536
+ className={props.className}
537
+ />
241
538
  );
242
- ExperimentList.displayName = "ExperimentList";
539
+ });
540
+ FailureList.displayName = "FailureList";
243
541
 
244
- /**
245
- * Eval 列表:每项一个 `experimentId + evalId`,固定展示判定、Attempt 数、分数、成本、耗时
246
- * 与失败原因;展开到这道题的 Attempt(与 AttemptList 同一份 AttemptListItem)。
247
- */
248
- export const EvalList: ReportComponent<EvalListProps> & { data: typeof evalListData } = Object.assign(
249
- defineComponent<EvalListProps>({
250
- web: (props, ctx) => <EvalListWeb {...props} locale={props.locale ?? ctx.locale} attemptHref={ctx.attemptHref} />,
251
- text: ({ items }, ctx) => evalListText(items, ctx),
252
- }),
253
- { data: evalListData },
254
- );
255
- EvalList.displayName = "EvalList";
542
+ // ───────────────────────── 指标组件 ─────────────────────────
256
543
 
257
- /**
258
- * Attempt 列表:实体列表的叶子层,每项一个 Attempt,固定展示判定、断言、error、Judge 评语
259
- * (assertions 的 detail/evidence)与证据引用(locator)。它不预设只看失败;
260
- * 报告作者过滤 `AttemptListItem[]`、用 `.slice()` 限量,`total` 让渲染面如实报告剩余数量。
261
- */
262
- export const AttemptList: ReportComponent<AttemptListProps> & { data: typeof attemptListData } = Object.assign(
263
- defineComponent<AttemptListProps>({
264
- web: (props, ctx) => <AttemptListWeb {...props} locale={props.locale ?? ctx.locale} attemptHref={ctx.attemptHref} />,
265
- text: ({ items, total }, ctx) => attemptListText(items, total, ctx),
266
- }),
267
- { data: attemptListData },
268
- );
269
- AttemptList.displayName = "AttemptList";
544
+ export type MetricTableProps = DataProps<
545
+ TableData,
546
+ MetricTableOptions,
547
+ ChromeProps & {
548
+ /** web 面在表格前渲染过滤输入框(enhance.js 接管);无 JS 时表格内容依旧完整。 */
549
+ filter?: boolean;
550
+ attemptHref?: (locator: AttemptLocator) => string;
551
+ }
552
+ >;
270
553
 
271
554
  /** 榜单:一行一个维度值、一列一个指标,回答「谁整体更好」。 */
272
- export const MetricTable: ReportComponent<MetricTableProps> & { data: typeof tableData } = Object.assign(
273
- defineComponent<MetricTableProps>({
274
- web: (props, ctx) => (
275
- <MetricTableWeb
276
- {...props}
277
- locale={props.locale ?? ctx.locale}
278
- attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)}
279
- />
280
- ),
281
- text: ({ data }, ctx) => tableText(data, ctx),
282
- }),
283
- { data: tableData },
284
- );
285
- MetricTable.displayName = "MetricTable";
555
+ export const MetricTable = makeDataComponent<
556
+ TableData,
557
+ MetricTableOptions,
558
+ ChromeProps & { filter?: boolean; attemptHref?: (locator: AttemptLocator) => string }
559
+ >({
560
+ name: "MetricTable",
561
+ dataFnName: "metricTableData",
562
+ shapeName: "TableData",
563
+ dataFn: metricTableData,
564
+ specKeys: ["rows", "columns", "sort", "evals"],
565
+ validate: validateTableData,
566
+ web: (props, ctx) => (
567
+ <MetricTableWeb
568
+ data={props.data}
569
+ filter={props.filter}
570
+ locale={props.locale ?? ctx.locale}
571
+ attemptHref={hrefOf(props, ctx)}
572
+ className={props.className}
573
+ />
574
+ ),
575
+ text: (props, ctx) => tableText(props.data, ctx),
576
+ }) as unknown as ReportComponent<MetricTableProps>;
577
+
578
+ export type MetricMatrixProps = DataProps<
579
+ MatrixData,
580
+ MetricMatrixOptions,
581
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
582
+ >;
583
+ export type MetricBarsProps = MetricMatrixProps;
286
584
 
287
585
  /** 逐题格子:行 × 列两个维度、格子里一个指标,回答「哪道题谁挂了」。 */
288
- export const MetricMatrix: ReportComponent<MetricMatrixProps> & { data: typeof matrixData } = Object.assign(
289
- defineComponent<MetricMatrixProps>({
290
- web: (props, ctx) => (
291
- <MetricMatrixWeb
292
- {...props}
293
- locale={props.locale ?? ctx.locale}
294
- attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)}
295
- />
296
- ),
297
- text: ({ data }, ctx) => matrixText(data, ctx),
298
- }),
299
- { data: matrixData },
300
- );
301
- MetricMatrix.displayName = "MetricMatrix";
302
-
303
- /** 分组条形:同一份矩阵数据的另一种摆法;MetricBars.data 就是 MetricMatrix.data 的别名。 */
304
- export const MetricBars: ReportComponent<MetricMatrixProps> & { data: typeof matrixData } = Object.assign(
305
- defineComponent<MetricMatrixProps>({
306
- web: (props, ctx) => (
307
- <MetricBarsWeb
308
- {...props}
309
- locale={props.locale ?? ctx.locale}
310
- attemptHref={props.attemptHref ?? (isHostWebContextActive() ? ctx.attemptHref : undefined)}
311
- />
312
- ),
313
- text: ({ data }) => barsText(data),
314
- }),
315
- { data: matrixData },
316
- );
317
- MetricBars.displayName = "MetricBars";
318
-
319
- /** 考试成绩单:总分 + 分科小计,固定分母、missing 如实报。 */
320
- export const Scoreboard: ReportComponent<ScoreboardProps> & { data: typeof scoreboardData } = Object.assign(
321
- defineComponent<ScoreboardProps>({
322
- web: (props, ctx) => <ScoreboardWeb {...props} locale={props.locale ?? ctx.locale} />,
323
- text: ({ data }, ctx) => scoreboardText(data, ctx),
324
- }),
325
- { data: scoreboardData },
326
- );
327
- Scoreboard.displayName = "Scoreboard";
328
-
329
- /** 质量 × 成本 frontier:每个点一个配置、两个指标各占一轴,「好」的角落恒在右上。 */
330
- export const MetricScatter: ReportComponent<MetricScatterProps> & { data: typeof scatterData } = Object.assign(
331
- defineComponent<MetricScatterProps, MetricScatterResolvedProps>({
332
- resolve: async (props) => {
333
- if ("data" in props && props.data !== undefined) return props;
334
- const { selection, points, series, x, y, ...rest } = props;
335
- return { ...rest, data: await scatterData(selection, { points, series, x, y }) };
336
- },
337
- web: (props, ctx) => {
338
- requireScatterData(props);
339
- return <MetricScatterWeb {...props} locale={props.locale ?? ctx.locale} />;
340
- },
341
- text: (props, ctx) => scatterText(requireScatterData(props), ctx),
342
- }),
343
- { data: scatterData },
344
- );
345
- MetricScatter.displayName = "MetricScatter";
346
-
347
- /** 趋势线:x 是 experiment 声明的 flag(flag()),同系列按 x 排序连线。 */
348
- export const MetricLine: ReportComponent<MetricLineProps> & { data: typeof lineData } = Object.assign(
349
- defineComponent<MetricLineProps>({
350
- web: (props, ctx) => <MetricLineWeb {...props} locale={props.locale ?? ctx.locale} />,
351
- text: ({ data }, ctx) => lineText(data, ctx),
352
- }),
353
- { data: lineData },
354
- );
355
- MetricLine.displayName = "MetricLine";
356
-
357
- /** 成对对比:每行一对配置、格子里 A、B、Δ 三个值,涨跌好坏由 better 判定。 */
358
- export const DeltaTable: ReportComponent<DeltaTableProps> & { data: typeof deltaData } = Object.assign(
359
- defineComponent<DeltaTableProps>({
360
- web: (props, ctx) => <DeltaTableWeb {...props} locale={props.locale ?? ctx.locale} />,
361
- text: ({ data }, ctx) => deltaText(data, ctx),
362
- }),
363
- { data: deltaData },
364
- );
365
- DeltaTable.displayName = "DeltaTable";
586
+ export const MetricMatrix = makeDataComponent<
587
+ MatrixData,
588
+ MetricMatrixOptions,
589
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
590
+ >({
591
+ name: "MetricMatrix",
592
+ dataFnName: "metricMatrixData",
593
+ shapeName: "MatrixData",
594
+ dataFn: metricMatrixData,
595
+ specKeys: ["rows", "columns", "cell", "evals"],
596
+ validate: validateMatrixData,
597
+ web: (props, ctx) => (
598
+ <MetricMatrixWeb data={props.data} locale={props.locale ?? ctx.locale} attemptHref={hrefOf(props, ctx)} className={props.className} />
599
+ ),
600
+ text: (props, ctx) => matrixText(props.data, ctx),
601
+ }) as unknown as ReportComponent<MetricMatrixProps>;
602
+
603
+ /** 分组条形:同一份矩阵数据的另一种摆法;与 MetricMatrix 写同一份 spec 时只计算一次。 */
604
+ export const MetricBars = makeDataComponent<
605
+ MatrixData,
606
+ MetricMatrixOptions,
607
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
608
+ >({
609
+ name: "MetricBars",
610
+ dataFnName: "metricMatrixData",
611
+ shapeName: "MatrixData",
612
+ dataFn: metricMatrixData,
613
+ specKeys: ["rows", "columns", "cell", "evals"],
614
+ validate: validateMatrixData,
615
+ web: (props, ctx) => (
616
+ <MetricBarsWeb data={props.data} locale={props.locale ?? ctx.locale} attemptHref={hrefOf(props, ctx)} className={props.className} />
617
+ ),
618
+ text: (props, ctx) => barsText(props.data, ctx),
619
+ }) as unknown as ReportComponent<MetricBarsProps>;
620
+
621
+ export type ScoreboardProps = DataProps<
622
+ ScoreboardData,
623
+ ScoreboardOptions,
624
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
625
+ >;
626
+
627
+ /** 考试成绩单:总分 + 分科小计,固定分母、notRun / unscorable 分开如实报。 */
628
+ export const Scoreboard = makeDataComponent<
629
+ ScoreboardData,
630
+ ScoreboardOptions,
631
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
632
+ >({
633
+ name: "Scoreboard",
634
+ dataFnName: "scoreboardData",
635
+ shapeName: "ScoreboardData",
636
+ dataFn: scoreboardData,
637
+ specKeys: ["rows", "questions", "subject", "weights", "fullMarks", "score"],
638
+ validate: validateScoreboardData,
639
+ web: (props, ctx) => <ScoreboardWeb data={props.data} locale={props.locale ?? ctx.locale} className={props.className} />,
640
+ text: (props, ctx) => scoreboardText(props.data, ctx),
641
+ }) as unknown as ReportComponent<ScoreboardProps>;
642
+
643
+ export type MetricScatterProps = DataProps<
644
+ ScatterData,
645
+ MetricScatterOptions,
646
+ ChromeProps & { pointHref?: (row: ScatterData["rows"][number]) => string }
647
+ >;
648
+
649
+ /** 两个指标之间的取舍:每个点一个维度值,x / y 各一个指标,series 只决定颜色和分组。 */
650
+ export const MetricScatter = makeDataComponent<
651
+ ScatterData,
652
+ MetricScatterOptions,
653
+ ChromeProps & { pointHref?: (row: ScatterData["rows"][number]) => string }
654
+ >({
655
+ name: "MetricScatter",
656
+ dataFnName: "metricScatterData",
657
+ shapeName: "ScatterData",
658
+ dataFn: metricScatterData,
659
+ specKeys: ["points", "series", "x", "y", "evals"],
660
+ validate: validateScatterData,
661
+ web: (props, ctx) => (
662
+ <MetricScatterWeb data={props.data} pointHref={props.pointHref} locale={props.locale ?? ctx.locale} className={props.className} />
663
+ ),
664
+ text: (props, ctx) => scatterText(props.data, ctx),
665
+ }) as unknown as ReportComponent<MetricScatterProps>;
666
+
667
+ export type MetricLineProps = DataProps<
668
+ LineData,
669
+ MetricLineOptions,
670
+ ChromeProps & { pointHref?: (row: LineData["rows"][number]) => string }
671
+ >;
672
+
673
+ /** 趋势线:x 是 NumericAxis(参数轴),点身份 = (series, x)。 */
674
+ export const MetricLine = makeDataComponent<
675
+ LineData,
676
+ MetricLineOptions,
677
+ ChromeProps & { pointHref?: (row: LineData["rows"][number]) => string }
678
+ >({
679
+ name: "MetricLine",
680
+ dataFnName: "metricLineData",
681
+ shapeName: "LineData",
682
+ dataFn: metricLineData,
683
+ specKeys: ["x", "series", "y", "evals"],
684
+ validate: validateLineData,
685
+ web: (props, ctx) => (
686
+ <MetricLineWeb data={props.data} pointHref={props.pointHref} locale={props.locale ?? ctx.locale} className={props.className} />
687
+ ),
688
+ text: (props, ctx) => lineText(props.data, ctx),
689
+ }) as unknown as ReportComponent<MetricLineProps>;
690
+
691
+ export type DeltaTableProps = DataProps<
692
+ DeltaData,
693
+ DeltaTableOptions,
694
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
695
+ >;
696
+
697
+ /** 成对对比:每行一对(字面声明或 pairsByFlag 派生),格子里 A、B、Δ 三个值。 */
698
+ export const DeltaTable = makeDataComponent<
699
+ DeltaData,
700
+ DeltaTableOptions,
701
+ ChromeProps & { attemptHref?: (locator: AttemptLocator) => string }
702
+ >({
703
+ name: "DeltaTable",
704
+ dataFnName: "deltaTableData",
705
+ shapeName: "DeltaData",
706
+ dataFn: deltaTableData,
707
+ specKeys: ["by", "pairs", "metrics", "evals"],
708
+ validate: validateDeltaData,
709
+ web: (props, ctx) => <DeltaTableWeb data={props.data} locale={props.locale ?? ctx.locale} className={props.className} />,
710
+ text: (props, ctx) => deltaText(props.data, ctx),
711
+ }) as unknown as ReportComponent<DeltaTableProps>;