niceeval 0.5.4 → 0.6.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 (342) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +5 -5
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +2 -2
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -1
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +3 -3
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  146. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +11 -11
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +12 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +129 -42
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +9 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +54 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/control-flow.ts +1 -1
  178. package/src/context/session.test.ts +92 -1
  179. package/src/context/session.ts +23 -1
  180. package/src/context/types.ts +4 -4
  181. package/src/define.test.ts +8 -8
  182. package/src/define.ts +11 -11
  183. package/src/expect/index.ts +5 -5
  184. package/src/i18n/en.ts +34 -15
  185. package/src/i18n/zh-CN.ts +37 -21
  186. package/src/index.ts +1 -1
  187. package/src/o11y/execution-tree.test.ts +452 -0
  188. package/src/o11y/execution-tree.ts +367 -0
  189. package/src/o11y/otlp/receiver.ts +1 -1
  190. package/src/o11y/otlp/select.ts +7 -3
  191. package/src/o11y/parsers/claude-code.test.ts +175 -0
  192. package/src/o11y/parsers/claude-code.ts +32 -0
  193. package/src/o11y/types.ts +6 -1
  194. package/src/report/aggregate.ts +26 -20
  195. package/src/report/built-in-user-parity.test.tsx +643 -0
  196. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  197. package/src/report/built-ins/index.ts +2 -0
  198. package/src/report/components.tsx +223 -50
  199. package/src/report/compute.ts +358 -53
  200. package/src/report/dual-render.test.tsx +1125 -0
  201. package/src/report/flag.ts +1 -1
  202. package/src/report/format.ts +50 -2
  203. package/src/report/index.ts +48 -14
  204. package/src/report/load.ts +1 -1
  205. package/src/report/locale.ts +206 -0
  206. package/src/report/metrics.ts +39 -15
  207. package/src/report/primitives.tsx +100 -2
  208. package/src/report/react/AttemptList.tsx +115 -0
  209. package/src/report/react/DeltaTable.tsx +9 -6
  210. package/src/report/react/EvalList.tsx +0 -0
  211. package/src/report/react/ExperimentList.tsx +108 -0
  212. package/src/report/react/GroupSummary.tsx +66 -0
  213. package/src/report/react/MetricBars.tsx +24 -13
  214. package/src/report/react/MetricLine.tsx +21 -21
  215. package/src/report/react/MetricMatrix.tsx +8 -4
  216. package/src/report/react/MetricScatter.tsx +136 -96
  217. package/src/report/react/MetricTable.tsx +115 -15
  218. package/src/report/react/RunOverview.tsx +48 -26
  219. package/src/report/react/Scoreboard.tsx +15 -7
  220. package/src/report/react/cell.tsx +18 -9
  221. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  222. package/src/report/react/colors.ts +24 -15
  223. package/src/report/react/enhance.js +129 -0
  224. package/src/report/react/fixtures.ts +163 -34
  225. package/src/report/react/format.ts +1 -1
  226. package/src/report/react/index.tsx +22 -6
  227. package/src/report/react/render.test.tsx +164 -31
  228. package/src/report/react/styles.css +405 -213
  229. package/src/report/report.test.ts +403 -94
  230. package/src/report/report.ts +29 -11
  231. package/src/report/text/faces.ts +303 -95
  232. package/src/report/text/layout.ts +76 -11
  233. package/src/report/text/table.ts +98 -0
  234. package/src/report/tree.ts +103 -20
  235. package/src/report/types.ts +196 -25
  236. package/src/report/web.ts +46 -19
  237. package/src/results/annotated-source.test.ts +146 -0
  238. package/src/results/annotated-source.ts +128 -0
  239. package/src/results/attempt-evidence.test.ts +158 -0
  240. package/src/results/attempt-evidence.ts +158 -0
  241. package/src/results/attempt-source.ts +48 -0
  242. package/src/results/copy.ts +154 -139
  243. package/src/results/format.ts +54 -47
  244. package/src/results/host-equivalence.test.ts +527 -0
  245. package/src/results/index.ts +56 -14
  246. package/src/results/locator.test.ts +198 -0
  247. package/src/results/locator.ts +0 -0
  248. package/src/results/open.ts +392 -175
  249. package/src/results/results.test.ts +943 -494
  250. package/src/results/select.ts +129 -23
  251. package/src/results/skipped-notice.ts +0 -0
  252. package/src/results/source-hash.ts +28 -0
  253. package/src/results/types.ts +98 -62
  254. package/src/results/writer.ts +304 -176
  255. package/src/runner/attempt.test.ts +170 -0
  256. package/src/runner/attempt.ts +48 -13
  257. package/src/runner/discover.test.ts +65 -0
  258. package/src/runner/discover.ts +6 -2
  259. package/src/runner/eval-source.test.ts +121 -0
  260. package/src/runner/eval-source.ts +45 -0
  261. package/src/runner/fingerprint.ts +54 -1
  262. package/src/runner/remote-sandbox.ts +1 -1
  263. package/src/runner/report.test.ts +6 -8
  264. package/src/runner/report.ts +5 -4
  265. package/src/runner/reporters/artifacts.ts +27 -41
  266. package/src/runner/reporters/braintrust.test.ts +3 -3
  267. package/src/runner/reporters/braintrust.ts +1 -1
  268. package/src/runner/reporters/console.ts +6 -6
  269. package/src/runner/reporters/json.ts +4 -4
  270. package/src/runner/reporters/live.test.ts +56 -0
  271. package/src/runner/reporters/live.ts +36 -10
  272. package/src/runner/reporters/quiet.test.ts +66 -0
  273. package/src/runner/reporters/quiet.ts +49 -0
  274. package/src/runner/reporters/shared.ts +5 -5
  275. package/src/runner/reporters/table.ts +17 -17
  276. package/src/runner/run.ts +61 -102
  277. package/src/runner/types.ts +43 -32
  278. package/src/sandbox/checkpoint.ts +1 -1
  279. package/src/sandbox/docker.ts +15 -2
  280. package/src/sandbox/e2b.ts +11 -5
  281. package/src/sandbox/errors.ts +13 -0
  282. package/src/sandbox/index.ts +3 -3
  283. package/src/sandbox/local-files.ts +1 -1
  284. package/src/sandbox/registry.ts +6 -5
  285. package/src/sandbox/resolve.ts +38 -23
  286. package/src/sandbox/retry.test.ts +68 -0
  287. package/src/sandbox/retry.ts +44 -0
  288. package/src/sandbox/types.ts +24 -24
  289. package/src/sandbox/vercel.ts +17 -3
  290. package/src/scoring/collector.ts +3 -3
  291. package/src/scoring/judge.ts +2 -2
  292. package/src/scoring/scoped.ts +13 -1
  293. package/src/scoring/types.ts +7 -7
  294. package/src/scoring/verdict.ts +4 -4
  295. package/src/shared/aggregate.ts +23 -3
  296. package/src/shared/types.ts +2 -2
  297. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  298. package/src/show/compose.ts +12 -132
  299. package/src/show/index.ts +125 -94
  300. package/src/show/render.ts +660 -160
  301. package/src/show/show.test.ts +472 -253
  302. package/src/source-loc.ts +1 -1
  303. package/src/tty-line.ts +32 -0
  304. package/src/types.ts +2 -2
  305. package/src/util.test.ts +21 -1
  306. package/src/util.ts +5 -1
  307. package/src/view/app/App.tsx +33 -198
  308. package/src/view/app/components/AttemptModal.tsx +6 -6
  309. package/src/view/app/components/CodeView.tsx +2 -2
  310. package/src/view/app/components/CopyControls.tsx +32 -36
  311. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  312. package/src/view/app/components/ui/badge.tsx +1 -1
  313. package/src/view/app/components/ui/dialog.tsx +2 -2
  314. package/src/view/app/i18n.ts +40 -31
  315. package/src/view/app/lib/artifact-url.ts +1 -1
  316. package/src/view/app/lib/attempt-route.test.ts +37 -47
  317. package/src/view/app/lib/attempt-route.ts +26 -26
  318. package/src/view/app/lib/rows.ts +9 -112
  319. package/src/view/app/lib/verdict.ts +25 -0
  320. package/src/view/app/main.tsx +9 -12
  321. package/src/view/app/pages/RunsPage.tsx +4 -4
  322. package/src/view/app/pages/TracesPage.tsx +3 -3
  323. package/src/view/app/shared.ts +0 -1
  324. package/src/view/app/types.ts +6 -51
  325. package/src/view/artifact-serving.test.ts +136 -0
  326. package/src/view/client-dist/app.css +1 -1
  327. package/src/view/client-dist/app.js +21 -21
  328. package/src/view/data.test.ts +214 -143
  329. package/src/view/data.ts +166 -120
  330. package/src/view/index.ts +23 -11
  331. package/src/view/server.ts +52 -17
  332. package/src/view/shared/types.ts +22 -31
  333. package/src/view/styles.css +117 -327
  334. package/src/view/view-report.test.ts +146 -98
  335. package/src/report/default-report.tsx +0 -222
  336. package/src/report/dual-face.test.tsx +0 -527
  337. package/src/report/react/CaseList.tsx +0 -70
  338. package/src/view/app/components/CostScoreChart.tsx +0 -147
  339. package/src/view/app/components/ExperimentTable.tsx +0 -284
  340. package/src/view/app/components/GroupSelector.tsx +0 -61
  341. package/src/view/app/components/primitives.tsx +0 -43
  342. package/src/view/app/lib/outcome.ts +0 -82
@@ -0,0 +1,37 @@
1
+ // web 宿主(view --report)的装载入口:同一棵树走 web 面,renderToStaticMarkup 吐静态
2
+ // HTML 烘进查看器的报告槽。只有这一侧真正 import react-dom(import 边界即运行时边界),
3
+ // 所以本文件不从 niceeval/report 的入口 re-export —— 宿主与测试按源路径 import。
4
+ import * as React from "react";
5
+ import { renderToStaticMarkup } from "react-dom/server";
6
+ import { resolveReportTree, runWithWebContext, validateReportTree } from "./tree.js";
7
+ import { DEFAULT_REPORT_LOCALE } from "./locale.js";
8
+ /**
9
+ * 挑选警告的 HTML 形态:宿主级前置块,与 RunOverview 里的警告用同一套结构和类名
10
+ * (`.nre nre-report-warnings` 外壳内一个 `ul.nre-warnings` + `li.nre-warning[data-kind]`,
11
+ * 复用 styles.css 已有的 `.nre .nre-warnings` 样式)。经 renderToStaticMarkup 走 React,
12
+ * message 文本自动转义,不裸拼 HTML。裸跑 / --report 都在报告顶上如实报残缺,不静默。
13
+ */
14
+ function renderSelectionWarningsHtml(warnings) {
15
+ return renderToStaticMarkup(React.createElement("div", { className: "nre nre-report-warnings" }, React.createElement("ul", { className: "nre-warnings" }, warnings.map((w, i) => React.createElement("li", { key: i, className: "nre-warning", "data-kind": w.kind }, w.message)))));
16
+ }
17
+ /**
18
+ * build → 渲染前解析数据组件(唯一的 await 边界)→ 树校验(与 text 宿主同一遍)→ 静态渲染
19
+ * web 面;Selection 有挑选警告时在报告顶部前置一块警告 HTML;报告树里的 RunOverview
20
+ * 已经渲染同一条时不重复。
21
+ */
22
+ export async function renderReportToStaticHtml(definition, ctx, options) {
23
+ const node = await definition.build(ctx);
24
+ const resolved = await resolveReportTree(node);
25
+ validateReportTree(resolved);
26
+ const webCtx = {
27
+ attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
28
+ locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
29
+ };
30
+ const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved));
31
+ const missingWarnings = ctx.selection.warnings.filter((warning) => {
32
+ const escapedMessage = renderToStaticMarkup(React.createElement(React.Fragment, null, warning.message));
33
+ return !body.includes(escapedMessage);
34
+ });
35
+ const warnings = missingWarnings.length > 0 ? renderSelectionWarningsHtml(missingWarnings) : "";
36
+ return warnings + body;
37
+ }
@@ -0,0 +1,61 @@
1
+ import type { AssertionResult } from "../types.ts";
2
+ /** 一行源码 + 映射到这一行的全部断言(保持原始顺序;可以是空数组)。 */
3
+ export interface AnnotatedSourceLine {
4
+ /** 1-indexed 行号,与 SourceLoc.line 同一坐标系。 */
5
+ line: number;
6
+ /** 这一行的原始文本(已按 normalizeEvalSource 归一化,不含行尾换行符)。 */
7
+ text: string;
8
+ /**
9
+ * 映射到这一行的断言,按输入顺序(通常即执行顺序)排列。每条断言自带 severity / score /
10
+ * passed / detail / evidence —— 这就是"状态 / 严重度 / 分数 / detail / evidence"标回源码行
11
+ * 的完整信息,不需要额外的行级折叠字段:折叠成单一显示态是 renderer 的展示决定,不是这份
12
+ * 领域 model 的职责(与 indexAsserts() 的先例一致,它同样只分桶、不折叠)。
13
+ */
14
+ assertions: AssertionResult[];
15
+ }
16
+ export interface AnnotatedEvalSourceSummary {
17
+ /** 参与统计的断言总数 = mappedAssertions + unmappedAssertions。 */
18
+ totalAssertions: number;
19
+ /** 成功映射到某一行源码的断言数。 */
20
+ mappedAssertions: number;
21
+ /** 落进 unmapped 桶的断言数。 */
22
+ unmappedAssertions: number;
23
+ /** 按 passed 计票(映射与未映射的断言都计入)。 */
24
+ passed: number;
25
+ failed: number;
26
+ /** 按 severity 计票(映射与未映射的断言都计入)。 */
27
+ gate: number;
28
+ soft: number;
29
+ /** 源码总行数,等于 lines.length。 */
30
+ totalLines: number;
31
+ /** 至少挂了一条断言的行数。 */
32
+ annotatedLines: number;
33
+ }
34
+ export interface AnnotatedEvalSource {
35
+ /** 项目相对路径,与 SourceArtifact.path / SourceLoc.file 同一约定。 */
36
+ sourcePath: string;
37
+ /** 归一化后源码文本的 SHA-256(与 captureEvalSource() 的算法一致,见 source-hash.ts)。 */
38
+ sourceSha256: string;
39
+ lines: AnnotatedSourceLine[];
40
+ /**
41
+ * 没有落到 lines 里任何一行的断言,原始顺序保留,永不丢弃。三种情况都进这里:
42
+ * 没有 SourceLoc(如 skip 前就出错的 eval、或调用方尚未收集到位置)、SourceLoc 指向
43
+ * 另一个文件(loc.file !== sourcePath —— 罕见,如断言写在共享 helper 里)、
44
+ * SourceLoc 的行号落在这份源码的行范围之外(源码与断言记录不同步的边界情况)。
45
+ * 后两种不是文档明确要求的场景,但"never silently dropped"的契约对它们同样成立 ——
46
+ * 这份 model 是分桶断言的唯一出口,不属于任何一行就必须出现在这里。
47
+ */
48
+ unmapped: AssertionResult[];
49
+ summary: AnnotatedEvalSourceSummary;
50
+ }
51
+ /**
52
+ * 纯函数:给定一份源码文本(未归一化也可以,内部会 normalizeEvalSource)与一批断言,
53
+ * 产出标注好的 AnnotatedEvalSource。幂等 / 无 IO —— 可以在渲染路径上安全调用。
54
+ *
55
+ * 断言 → 源码行的映射规则:`a.loc` 存在、`a.loc.file === source.path`、且
56
+ * `1 <= a.loc.line <= lines.length` 三者同时成立才落到对应行;否则进 `unmapped`。
57
+ */
58
+ export declare function buildAnnotatedEvalSource(source: {
59
+ path: string;
60
+ content: string;
61
+ }, assertions: readonly AssertionResult[]): AnnotatedEvalSource;
@@ -0,0 +1,69 @@
1
+ import type { AttemptHandle } from "./types.ts";
2
+ import { type AttemptIdentity, type AttemptLocator } from "./locator.ts";
3
+ import type { AnnotatedEvalSource } from "./annotated-source.ts";
4
+ import { type ExecutionTree } from "../o11y/execution-tree.ts";
5
+ import type { DiffData, EvalResult } from "../types.ts";
6
+ /**
7
+ * 一个 Attempt 的 artifact 落盘位置。单一目录足够:一个 attempt 的全部 artifact
8
+ * (events.json / trace.json / o11y.json / diff.json / sources.json,见 ARTIFACT_KINDS)
9
+ * 要么全在它自己的 attempt 目录,要么(--resume 携带条目——本轮没写任何新数据,
10
+ * 见 writer.ts 对携带分支的说明)全在 artifactBase 回退到的原快照 attempt 目录,
11
+ * 从不出现"这个 kind 在自己目录、那个 kind 在 artifactBase"的拆分场景,不需要
12
+ * 按 kind 逐个给路径。想要某一种 artifact 的具体文件,自己拼
13
+ * `join(artifactPaths.dir, "<kind>.json")`(kind 见 `ArtifactKind`)。
14
+ *
15
+ * 与 show/render.ts 的 `attemptArtifactsPath(attempt, cwd)` 是同一份"这个 attempt 的证据在哪"
16
+ * 知识的两种呈现:那边为 CLI 文本展示把它转成 cwd 相对的短路径,这里给 evidence 层的调用方
17
+ * (view / 静态导出 / 报告列表,它们没有统一的"cwd"概念)一份绝对路径,不做相对化。
18
+ */
19
+ export interface EvidencePaths {
20
+ /** 绝对路径。 */
21
+ dir: string;
22
+ }
23
+ /**
24
+ * 每一位只在数据真的存在且非空时为 true——从不因为对应的 artifact 文件"存在"就点亮,
25
+ * 见本文件头注逐位的门槛说明。show/view/静态导出/报告列表读这四位来决定要不要显示
26
+ * 对应的证据切面,不自己重新判断"这个 attempt 有没有 X"。
27
+ */
28
+ export interface AttemptEvidenceCapabilities {
29
+ /** evalSource 非空且有内容(源码行或断言二选一非空)。 */
30
+ eval: boolean;
31
+ /** events 非空数组——有骨架可看。 */
32
+ execution: boolean;
33
+ /** execution 成立且这次运行接入过 OTel(ExecutionTree.timingAvailable)。 */
34
+ timing: boolean;
35
+ /** diff 非空且至少改动/删除了一个文件。 */
36
+ diff: boolean;
37
+ }
38
+ /**
39
+ * 一个 Attempt 的完整证据聚合。除 `locator` 外每个字段要么是别的模块已经产出的纯数据类型,
40
+ * 要么是这四样拼出来的判定——本类型自己不发明新的证据形状。
41
+ */
42
+ export interface AttemptEvidence {
43
+ locator: AttemptLocator;
44
+ /** locator 派生自的不可变身份元组,与 `locator` 编码的是同一份数据,供不想解码 locator
45
+ * 的调用方直接读结构化字段(如按 experimentId 分组、按时间排序)。 */
46
+ identity: AttemptIdentity;
47
+ result: EvalResult;
48
+ /** 运行时保存的 Eval 源码标注(`--eval` 切面);没有 sources() 就是 null,不伪造空文档。 */
49
+ evalSource: AnnotatedEvalSource | null;
50
+ /** 标准事件流 + OTel enrichment(`--execution` 切面);没有非空 events 就是 null,
51
+ * 不产出一棵只剩 telemetry-only 节点、没有真实骨架的误导性树(见头注 execution 门槛)。 */
52
+ execution: ExecutionTree | null;
53
+ /** 被测 Agent 对 Sandbox 工作区的文件变化(`--diff` 切面);原样透传 attempt.diff()。 */
54
+ diff: DiffData | null;
55
+ artifactPaths: EvidencePaths;
56
+ capabilities: AttemptEvidenceCapabilities;
57
+ }
58
+ /**
59
+ * 纯组合:一次性 await 好 attempt 的四类懒加载证据(events / trace / diff / 经
60
+ * loadAnnotatedEvalSource 解引用的 eval 源码),拼成一份 AttemptEvidence。不重新实现
61
+ * Eval 源码标注、ExecutionTree 合并或 diff 语义的任何一条规则——那些规则的家分别在
62
+ * annotated-source.ts / execution-tree.ts / attempt.diff() 自身,这里只调用与判定"够不够
63
+ * 亮起对应的证据切面"。
64
+ *
65
+ * `attempt.locator` 在真实读取路径(openResults() 产出的 handle)恒有值;缺失时按当前身份
66
+ * 元组按 encodeAttemptLocator 兜底算一份,同 open.ts 给 record.locator 做的兜底同一口径——
67
+ * 只服务手工构造 AttemptHandle 的测试场景,不改变真实读取路径的行为。
68
+ */
69
+ export declare function loadAttemptEvidence(attempt: AttemptHandle): Promise<AttemptEvidence>;
@@ -0,0 +1,15 @@
1
+ import type { AttemptHandle } from "./types.ts";
2
+ import { type AnnotatedEvalSource } from "./annotated-source.ts";
3
+ /**
4
+ * 给定一个 attempt,取回它的 eval 源码(经 sources() 解引用,可能来自本快照或
5
+ * artifactBase 回退到的原快照)与它的断言,产出标注好的 AnnotatedEvalSource。
6
+ *
7
+ * 没有 sources()(eval 从没被 `loc` 引用到、或落盘里压根没有 sources.json/artifactBase
8
+ * 已失效)时返回 null——没有源码就没有「标注源码」这回事,不伪造一份空文档。
9
+ *
10
+ * 一个 attempt 的 sources() 理论上可以有多份文件(主 eval 文件 + 罕见的跨文件 loc);
11
+ * 这里按「哪份文件被最多断言的 loc 命中」选一份作为主文件喂给 buildAnnotatedEvalSource——
12
+ * 只服务这条打通链路的验证目的,不是最终选择策略(后续阶段如需要为每份引用到的文件
13
+ * 都出一份 AnnotatedEvalSource,在这基础上很容易扩成返回数组)。
14
+ */
15
+ export declare function loadAnnotatedEvalSource(attempt: AttemptHandle): Promise<AnnotatedEvalSource | null>;
@@ -0,0 +1,75 @@
1
+ /** 不透明的 Attempt 定位符:`@` + 1 位 scheme 字符 + 定长 base36 body。字符串品牌,防止和普通 string 混用。 */
2
+ export type AttemptLocator = string & {
3
+ readonly __brand: "AttemptLocator";
4
+ };
5
+ /** locator 派生自的不可变身份元组;attempt 是 EvalResult.attempt 的 0-indexed 值,不是展示用的 1-indexed 序号。 */
6
+ export interface AttemptIdentity {
7
+ experimentId: string;
8
+ /** SnapshotMeta.startedAt 字段(不是快照目录名——两者通常但不总是相等,见 writer.ts 的说明)。 */
9
+ snapshotStartedAt: string;
10
+ evalId: string;
11
+ /** 0-indexed;展示层(CLI/show)在边界处 +1,身份本身永远用内部下标。 */
12
+ attempt: number;
13
+ }
14
+ /** locator 前缀,恒为 `@`;与 Eval id 前缀在 CLI 位置参数解析里无歧义(eval id 不以 `@` 起头)。 */
15
+ export declare const ATTEMPT_LOCATOR_PREFIX = "@";
16
+ /**
17
+ * 确定性、带版本编码:同一身份元组永远产出同一 locator;scheme 版本混进哈希输入,
18
+ * 一旦编码规则升级(scheme 号递增),新旧两代 locator 天然落在不同哈希空间,不会互相撞车。
19
+ */
20
+ export declare function encodeAttemptLocator(identity: AttemptIdentity): AttemptLocator;
21
+ /** decodeAttemptLocator 的结果:locator 是身份的单向哈希,这里只判断字符串本身合不合法,不还原身份元组。 */
22
+ export type LocatorDecodeResult = {
23
+ valid: true;
24
+ scheme: number;
25
+ } | {
26
+ valid: false;
27
+ reason: string;
28
+ };
29
+ /**
30
+ * 语法校验:`@` 前缀 + scheme 字符 + body。不查 body 是否真对应某个已知 Attempt——
31
+ * 那是 reader 建好的 locator → AttemptHandle 索引的事(resolveAttemptLocator)。
32
+ */
33
+ export declare function decodeAttemptLocator(locator: string): LocatorDecodeResult;
34
+ /** buildLocatorIndex 的输入项:一个已知 Attempt 的身份 + 调用方自己的句柄类型(通常是 AttemptHandle)。 */
35
+ export interface LocatorAttempt<T> {
36
+ identity: AttemptIdentity;
37
+ handle: T;
38
+ }
39
+ /**
40
+ * 撞车:两个不同的身份元组独立编码出了同一个 locator 字符串。设计上应当极其罕见
41
+ * (见 BODY_LENGTH 的空间量级),但绝不能被静默吞掉——reader 打开结果根时一旦命中,
42
+ * 直接抛出,交给上层(未来的 CLI 层)决定怎么给用户看。
43
+ */
44
+ export declare class LocatorCollisionError extends Error {
45
+ readonly locator: AttemptLocator;
46
+ readonly identities: readonly [AttemptIdentity, AttemptIdentity];
47
+ constructor(locator: AttemptLocator, identities: readonly [AttemptIdentity, AttemptIdentity]);
48
+ }
49
+ /**
50
+ * 批量建 locator → 句柄索引。同一身份元组出现多次(如 --resume 携带条目在新旧两个快照里
51
+ * 都能被扫描到,身份沿用原 Attempt 不变)不算撞车,后出现的覆盖先出现的;真正撞车
52
+ * ——两个不同身份编码同一个字符串——才抛 LocatorCollisionError。
53
+ *
54
+ * @param encode @internal 仅供测试注入确定性会撞车的编码函数;生产调用不传,用默认实现。
55
+ */
56
+ export declare function buildLocatorIndex<T>(attempts: Iterable<LocatorAttempt<T>>, encode?: (identity: AttemptIdentity) => AttemptLocator): Map<AttemptLocator, T>;
57
+ /** resolveAttemptLocator 的结果:found / malformed / not-found 三种失败模式互相区分,不折叠成一个 Error。 */
58
+ export type LocatorResolution<T> = {
59
+ kind: "found";
60
+ locator: AttemptLocator;
61
+ handle: T;
62
+ } | {
63
+ kind: "malformed";
64
+ input: string;
65
+ reason: string;
66
+ } | {
67
+ kind: "not-found";
68
+ locator: AttemptLocator;
69
+ };
70
+ /**
71
+ * 拿用户输入的原始字符串(通常来自 CLI 的 `@...` 位置参数)在已建好的索引里查找。
72
+ * 先语法校验(decodeAttemptLocator),语法都不对就不必查索引;语法对但索引里没有 ——
73
+ * 已损坏、已过期(指向被清理的快照)、或纯粹打错——一律 not-found,由调用方决定怎么提示。
74
+ */
75
+ export declare function resolveAttemptLocator<T>(index: ReadonlyMap<AttemptLocator, T>, input: string): LocatorResolution<T>;
Binary file
@@ -0,0 +1,45 @@
1
+ import type { AttemptHandle, DedupeWarning, Experiment, Results, Selection, SelectionWarning, Snapshot } from "./types.ts";
2
+ /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
3
+ export declare function selectLatest(experiments: Experiment[], opts?: {
4
+ experiments?: string | string[];
5
+ }): Selection;
6
+ /** selectCurrentResults 的范围输入:experiment id 前缀与 eval id 前缀,都可缺省。 */
7
+ export interface ResultScope {
8
+ /** experiment id 前缀(--experiment),分段匹配语义同 filterExperiments。 */
9
+ experiment?: string;
10
+ /** eval id 前缀(位置参数),收窄 Selection 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
11
+ patterns?: string[];
12
+ }
13
+ /**
14
+ * 两个宿主(show / view)共用的现刻水位选择器:每个 experiment × eval 取时间上最新的那份
15
+ * 判定,跨 run 合成。results.latest() 只挑「每实验最新快照」,带 eval 前缀的局部重跑会产出
16
+ * 残缺快照;现刻水位承诺「不会因为一次局部重跑变残缺」,所以在实验的全部历史快照上逐 eval
17
+ * 向更早的 run 补齐,再把合成好的 Selection 交给宿主注入报告槽——内置默认报告与 --report 吃
18
+ * 同一份。
19
+ *
20
+ * 同一 eval 的全部 attempts 必须整批取自包含它的最新快照,不把历史快照的 attempts 平铺后
21
+ * 按 eval 聚合——否则会把不同运行的重试混成一次虚构运行。合成快照的 dir/元数据只服务报告
22
+ * 分组与来源展示,证据身份一律来自 attempt 自己的 ref。
23
+ * 警告随 Selection 重算:partial-coverage 的分母 = 已知并集 ∩ 范围(范围收窄时分母同步收窄,
24
+ * 不让范围外的缺口刷屏);stale / unfinished 与 results.latest() 同口径。
25
+ */
26
+ export declare function selectCurrentResults(results: Results, scope?: ResultScope): Selection;
27
+ /**
28
+ * Selection 构造:filter 只删不换 —— 快照删减,warnings 修剪规则是
29
+ * 「experimentId 不在幸存快照中的丢弃,非实验作用域的保留」(为将来非 per-experiment 的 kind 留位置)。
30
+ */
31
+ export declare function makeSelection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection;
32
+ /**
33
+ * 跨快照聚合前的身份键去重:(experimentId, evalId, attempt, startedAt)。
34
+ * --resume 会把上一轮已通过的结果原样合入新快照,同一 attempt 因此存在于多份落盘;
35
+ * 重复时保留最新快照里的那份(内容相同,取新快照的副本让 ref 落在最新落盘上;
36
+ * 位置取首次出现处,顺序稳定)。startedAt 缺失时宁可不去重也不误删,记入 warnings。
37
+ */
38
+ export declare function dedupeAttempts(attempts: AttemptHandle[]): {
39
+ attempts: AttemptHandle[];
40
+ warnings: DedupeWarning[];
41
+ };
42
+ /** 快照新旧比较:startedAt 优先,同刻按快照目录名(时间戳 + 随机后缀,字典序即时序)。 */
43
+ export declare function isNewerSnapshot(a: Snapshot, b: Snapshot): boolean;
44
+ /** experiment id 分段前缀过滤(--experiment / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
45
+ export declare function filterExperiments(experiments: Experiment[], filter?: string | string[]): Experiment[];
@@ -0,0 +1,230 @@
1
+ // 快照 Selection 与 attempt 去重(定稿见 docs/feature/results/library.md「选择快照」「身份键与去重」)。
2
+ //
3
+ // 选择器只有一个(latest),长在集合上;它不是 DSL,只是最常用的那次筛选。
4
+ // 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Selection 走 ——
5
+ // 渲染与否在消费方(message 是渲染好的英文句子),但缺口不静默。
6
+ import { evalPrefixPredicate } from "../shared/aggregate.js";
7
+ /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
8
+ export function selectLatest(experiments, opts) {
9
+ const selected = filterExperiments(experiments, opts?.experiments);
10
+ const snapshots = selected.map((exp) => exp.latest);
11
+ const warnings = [];
12
+ // stale 的基准:Selection 中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)。
13
+ let latestStartedAt = "";
14
+ for (const snapshot of snapshots) {
15
+ if (snapshot.startedAt > latestStartedAt)
16
+ latestStartedAt = snapshot.startedAt;
17
+ }
18
+ for (const exp of selected) {
19
+ const snapshot = exp.latest;
20
+ // 残缺检测:分母 = 该实验已知 eval 并集(本地历史 ∪ 各快照携带的 knownEvalIds)。
21
+ // 位置参数允许只重跑一道题,产出的「最新快照」可能只有一道题 —— 不能安静吞下。
22
+ const covered = snapshot.evals.length;
23
+ const total = exp.evalIds.length;
24
+ if (covered < total) {
25
+ warnings.push({
26
+ kind: "partial-coverage",
27
+ experimentId: exp.id,
28
+ covered,
29
+ total,
30
+ message: `snapshot covers ${covered} of ${total} evals seen in history; re-run \`niceeval exp ${exp.id}\` for a full snapshot`,
31
+ });
32
+ }
33
+ if (snapshot.startedAt < latestStartedAt) {
34
+ warnings.push({
35
+ kind: "stale-snapshot",
36
+ experimentId: exp.id,
37
+ startedAt: snapshot.startedAt,
38
+ latestStartedAt,
39
+ message: `snapshot "${exp.id}" (${snapshot.startedAt}) predates the latest run in this selection by ${humanizeGap(snapshot.startedAt, latestStartedAt)}`,
40
+ });
41
+ }
42
+ if (!snapshot.completedAt) {
43
+ warnings.push({
44
+ kind: "unfinished-snapshot",
45
+ experimentId: exp.id,
46
+ startedAt: snapshot.startedAt,
47
+ dir: snapshot.dir,
48
+ message: `snapshot "${exp.id}" (${snapshot.startedAt}) has no completedAt — the run was interrupted; results may be incomplete`,
49
+ });
50
+ }
51
+ }
52
+ return makeSelection(snapshots, warnings);
53
+ }
54
+ /**
55
+ * 两个宿主(show / view)共用的现刻水位选择器:每个 experiment × eval 取时间上最新的那份
56
+ * 判定,跨 run 合成。results.latest() 只挑「每实验最新快照」,带 eval 前缀的局部重跑会产出
57
+ * 残缺快照;现刻水位承诺「不会因为一次局部重跑变残缺」,所以在实验的全部历史快照上逐 eval
58
+ * 向更早的 run 补齐,再把合成好的 Selection 交给宿主注入报告槽——内置默认报告与 --report 吃
59
+ * 同一份。
60
+ *
61
+ * 同一 eval 的全部 attempts 必须整批取自包含它的最新快照,不把历史快照的 attempts 平铺后
62
+ * 按 eval 聚合——否则会把不同运行的重试混成一次虚构运行。合成快照的 dir/元数据只服务报告
63
+ * 分组与来源展示,证据身份一律来自 attempt 自己的 ref。
64
+ * 警告随 Selection 重算:partial-coverage 的分母 = 已知并集 ∩ 范围(范围收窄时分母同步收窄,
65
+ * 不让范围外的缺口刷屏);stale / unfinished 与 results.latest() 同口径。
66
+ */
67
+ export function selectCurrentResults(results, scope = {}) {
68
+ const match = scope.patterns && scope.patterns.length > 0 ? evalPrefixPredicate(scope.patterns) : () => true;
69
+ const experiments = filterExperiments(results.experiments, scope.experiment);
70
+ const snapshots = [];
71
+ const warnings = [];
72
+ for (const exp of experiments) {
73
+ // 逐题取最新:快照按最新在前,首个出现即最新判定
74
+ const taken = new Map();
75
+ for (const snapshot of exp.snapshots) {
76
+ for (const ev of snapshot.evals) {
77
+ if (!match(ev.id) || taken.has(ev.id))
78
+ continue;
79
+ taken.set(ev.id, { ev, snapshot });
80
+ }
81
+ }
82
+ if (taken.size === 0)
83
+ continue;
84
+ const picks = [...taken.values()].sort((a, b) => a.ev.id.localeCompare(b.ev.id));
85
+ let startedAt = "";
86
+ let newest = picks[0].snapshot;
87
+ for (const pick of picks) {
88
+ if (pick.snapshot.startedAt > startedAt) {
89
+ startedAt = pick.snapshot.startedAt;
90
+ newest = pick.snapshot;
91
+ }
92
+ }
93
+ const evals = picks.map((p) => p.ev);
94
+ const base = exp.latest;
95
+ snapshots.push({
96
+ experimentId: exp.id,
97
+ startedAt,
98
+ agent: base.agent,
99
+ ...(base.model !== undefined ? { model: base.model } : {}),
100
+ producer: base.producer,
101
+ schemaVersion: base.schemaVersion,
102
+ evals,
103
+ attempts: evals.flatMap((ev) => ev.attempts),
104
+ dir: newest.dir,
105
+ ...(newest.completedAt !== undefined ? { completedAt: newest.completedAt } : {}),
106
+ ...(base.knownEvalIds ? { knownEvalIds: [...base.knownEvalIds] } : {}),
107
+ });
108
+ // 残缺检测:跨快照补齐后仍缺,只可能是历史上见过(或 knownEvalIds 声明过)
109
+ // 却从未在可读落盘里出现的题 —— 分母收窄到范围内,不让范围外的缺口刷屏。
110
+ const total = exp.evalIds.filter(match).length;
111
+ if (evals.length < total) {
112
+ warnings.push({
113
+ kind: "partial-coverage",
114
+ experimentId: exp.id,
115
+ covered: evals.length,
116
+ total,
117
+ message: `verdicts cover ${evals.length} of ${total} evals seen in history; re-run \`niceeval exp ${exp.id}\` for a full snapshot`,
118
+ });
119
+ }
120
+ }
121
+ let latestStartedAt = "";
122
+ for (const snapshot of snapshots) {
123
+ if (snapshot.startedAt > latestStartedAt)
124
+ latestStartedAt = snapshot.startedAt;
125
+ }
126
+ for (const snapshot of snapshots) {
127
+ if (snapshot.startedAt < latestStartedAt) {
128
+ warnings.push({
129
+ kind: "stale-snapshot",
130
+ experimentId: snapshot.experimentId,
131
+ startedAt: snapshot.startedAt,
132
+ latestStartedAt,
133
+ message: `verdicts for "${snapshot.experimentId}" were produced at ${snapshot.startedAt}, before the latest run in this selection (${latestStartedAt})`,
134
+ });
135
+ }
136
+ if (snapshot.completedAt === undefined) {
137
+ warnings.push({
138
+ kind: "unfinished-snapshot",
139
+ experimentId: snapshot.experimentId,
140
+ startedAt: snapshot.startedAt,
141
+ dir: snapshot.dir,
142
+ message: `snapshot "${snapshot.experimentId}" (${snapshot.startedAt}) is unfinished (the process was interrupted); completed attempts are read as-is, but the set may be incomplete`,
143
+ });
144
+ }
145
+ }
146
+ return makeSelection(snapshots, warnings);
147
+ }
148
+ /**
149
+ * Selection 构造:filter 只删不换 —— 快照删减,warnings 修剪规则是
150
+ * 「experimentId 不在幸存快照中的丢弃,非实验作用域的保留」(为将来非 per-experiment 的 kind 留位置)。
151
+ */
152
+ export function makeSelection(snapshots, warnings) {
153
+ return {
154
+ snapshots,
155
+ warnings,
156
+ filter(predicate) {
157
+ const kept = snapshots.filter(predicate);
158
+ const survivors = new Set(kept.map((s) => s.experimentId));
159
+ const keptWarnings = warnings.filter((w) => {
160
+ const scope = w.experimentId;
161
+ return typeof scope !== "string" || survivors.has(scope);
162
+ });
163
+ return makeSelection(kept, keptWarnings);
164
+ },
165
+ };
166
+ }
167
+ /**
168
+ * 跨快照聚合前的身份键去重:(experimentId, evalId, attempt, startedAt)。
169
+ * --resume 会把上一轮已通过的结果原样合入新快照,同一 attempt 因此存在于多份落盘;
170
+ * 重复时保留最新快照里的那份(内容相同,取新快照的副本让 ref 落在最新落盘上;
171
+ * 位置取首次出现处,顺序稳定)。startedAt 缺失时宁可不去重也不误删,记入 warnings。
172
+ */
173
+ export function dedupeAttempts(attempts) {
174
+ const deduped = [];
175
+ const indexByKey = new Map();
176
+ const warnings = [];
177
+ for (const attempt of attempts) {
178
+ const r = attempt.result;
179
+ if (!r.startedAt) {
180
+ warnings.push({
181
+ kind: "missing-startedAt",
182
+ experimentId: attempt.experimentId,
183
+ evalId: attempt.evalId,
184
+ message: `attempt ${r.attempt} of eval "${attempt.evalId}" in experiment "${attempt.experimentId}" has no startedAt; kept as-is without dedupe`,
185
+ });
186
+ deduped.push(attempt);
187
+ continue;
188
+ }
189
+ const key = JSON.stringify([attempt.experimentId, r.id, r.attempt, r.startedAt]);
190
+ const existing = indexByKey.get(key);
191
+ if (existing === undefined) {
192
+ indexByKey.set(key, deduped.length);
193
+ deduped.push(attempt);
194
+ }
195
+ else if (isNewerSnapshot(attempt.snapshot, deduped[existing].snapshot)) {
196
+ deduped[existing] = attempt;
197
+ }
198
+ }
199
+ return { attempts: deduped, warnings };
200
+ }
201
+ /** 快照新旧比较:startedAt 优先,同刻按快照目录名(时间戳 + 随机后缀,字典序即时序)。 */
202
+ export function isNewerSnapshot(a, b) {
203
+ const byStart = a.startedAt.localeCompare(b.startedAt);
204
+ if (byStart !== 0)
205
+ return byStart > 0;
206
+ return a.dir.localeCompare(b.dir) > 0;
207
+ }
208
+ /** experiment id 分段前缀过滤(--experiment / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
209
+ export function filterExperiments(experiments, filter) {
210
+ if (filter === undefined)
211
+ return experiments;
212
+ // 允许 "compare/" 这种带尾斜杠的写法,与 "compare" 等价;分段匹配不误配 "compare2"。
213
+ const prefixes = (Array.isArray(filter) ? filter : [filter]).map((p) => p.replace(/\/+$/, ""));
214
+ return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
215
+ }
216
+ /** stale 警告的人话时距:选粒度最大的单位,四舍五入。 */
217
+ function humanizeGap(fromIso, toIso) {
218
+ const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
219
+ const seconds = Math.round(ms / 1000);
220
+ if (seconds < 90)
221
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
222
+ const minutes = Math.round(seconds / 60);
223
+ if (minutes < 90)
224
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
225
+ const hours = Math.round(minutes / 60);
226
+ if (hours < 36)
227
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
228
+ const days = Math.round(hours / 24);
229
+ return `${days} day${days === 1 ? "" : "s"}`;
230
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 归一化 eval 源码文本:去 UTF-8 BOM、把 CRLF / CR 统一成 LF。
3
+ * 幂等——已归一化的文本再跑一次结果不变,所以 capture 时归一化一次、重建时可以放心再跑一次,
4
+ * 两处算出的 SHA-256 恒相同。不做其它改写(不 trim 行尾空白、不折叠空行):
5
+ * 行号是断言 SourceLoc 的锚,任何改变行结构的归一化都会让映射错位。
6
+ */
7
+ export declare function normalizeEvalSource(text: string): string;
8
+ /** 对(已归一化的)源码文本算 SHA-256 十六进制摘要。 */
9
+ export declare function hashEvalSource(content: string): string;