niceeval 0.6.2 → 0.7.1

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 (218) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/lib/artifact-url.ts +14 -3
  179. package/src/view/app/main.tsx +13 -8
  180. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  181. package/src/view/app/types.ts +4 -1
  182. package/src/view/artifact-serving.test.ts +21 -1
  183. package/src/view/client-dist/app.css +1 -1
  184. package/src/view/client-dist/app.js +6 -6
  185. package/src/view/data.test.ts +9 -3
  186. package/src/view/data.ts +145 -49
  187. package/src/view/index.ts +48 -44
  188. package/src/view/server.ts +35 -15
  189. package/src/view/shared/types.ts +34 -5
  190. package/src/view/styles.css +224 -0
  191. package/src/view/view-report.test.ts +161 -57
  192. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  193. package/dist/report/built-ins/experiment-comparison.js +0 -119
  194. package/dist/report/built-ins/index.d.ts +0 -2
  195. package/dist/report/built-ins/index.js +0 -2
  196. package/dist/report/react/GroupSummary.d.ts +0 -8
  197. package/dist/report/react/GroupSummary.js +0 -8
  198. package/dist/report/react/RunOverview.d.ts +0 -8
  199. package/dist/report/react/RunOverview.js +0 -12
  200. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  201. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  202. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  203. package/docs-site/zh/example/showcase.mdx +0 -39
  204. package/src/report/built-in-user-parity.test.tsx +0 -597
  205. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  206. package/src/report/built-ins/index.ts +0 -7
  207. package/src/report/react/GroupSummary.tsx +0 -66
  208. package/src/report/react/RunOverview.tsx +0 -109
  209. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  213. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  214. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  217. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  218. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
package/src/report/web.ts CHANGED
@@ -6,12 +6,20 @@ import * as React from "react";
6
6
  import type { ReactNode } from "react";
7
7
  import { renderToStaticMarkup } from "react-dom/server";
8
8
  import type { AttemptLocator } from "../results/locator.ts";
9
- import type { SelectionWarning } from "../results/types.ts";
10
- import { resolveReportTree, runWithWebContext, validateReportTree, type WebContext } from "./tree.ts";
9
+ import type { Scope } from "../results/types.ts";
10
+ import {
11
+ resolveReportTree,
12
+ runWithWebContext,
13
+ validateReportTree,
14
+ ResolveMemo,
15
+ type WebContext,
16
+ } from "./tree.ts";
11
17
  import { DEFAULT_REPORT_LOCALE, type ReportLocale } from "./locale.ts";
12
- import type { ReportContext, ReportDefinition } from "./report.ts";
18
+ import { buildReportMeta, pickReportPage, type ReportDefinition, type ReportHostContext } from "./report.ts";
13
19
 
14
20
  export interface StaticHtmlOptions {
21
+ /** 渲染哪一页;缺省第一页。未命中抛 ReportPageNotFoundError。 */
22
+ pageId?: string;
15
23
  /** 证据室深链;缺省用 view 的 attempt 路由 `#/attempt/@<locator>`(单段、不透明)。 */
16
24
  attemptHref?: (locator: AttemptLocator) => string;
17
25
  /** 官方组件 chrome 文案的 locale;默认 "en"。 */
@@ -19,12 +27,12 @@ export interface StaticHtmlOptions {
19
27
  }
20
28
 
21
29
  /**
22
- * 挑选警告的 HTML 形态:宿主级前置块,与 RunOverview 里的警告用同一套结构和类名
23
- * (`.nre nre-report-warnings` 外壳内一个 `ul.nre-warnings` + `li.nre-warning[data-kind]`,
24
- * 复用 styles.css 已有的 `.nre .nre-warnings` 样式)。经 renderToStaticMarkup 走 React,
25
- * message 文本自动转义,不裸拼 HTML。裸跑 / --report 都在报告顶上如实报残缺,不静默。
30
+ * 挑选警告的 HTML 形态:宿主级前置块(`.nre nre-report-warnings` 外壳内一个
31
+ * `ul.nre-warnings` + `li.nre-warning[data-kind]`,复用 styles.css 已有样式)。
32
+ * `command` 的警告把命令渲染为可复制块(`.nre-warning-command`);无 command
33
+ * 只显示 message,不硬造动作。经 renderToStaticMarkup React,文本自动转义。
26
34
  */
27
- function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
35
+ function renderScopeWarningsHtml(scope: Scope): string {
28
36
  return renderToStaticMarkup(
29
37
  React.createElement(
30
38
  "div",
@@ -32,8 +40,15 @@ function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
32
40
  React.createElement(
33
41
  "ul",
34
42
  { className: "nre-warnings" },
35
- warnings.map((w, i) =>
36
- React.createElement("li", { key: i, className: "nre-warning", "data-kind": w.kind }, w.message),
43
+ scope.warnings.map((w, i) =>
44
+ React.createElement(
45
+ "li",
46
+ { key: i, className: "nre-warning", "data-kind": w.kind },
47
+ w.message,
48
+ "command" in w && w.command
49
+ ? React.createElement("code", { className: "nre-warning-command", "data-nre-copy": w.command }, w.command)
50
+ : null,
51
+ ),
37
52
  ),
38
53
  ),
39
54
  ),
@@ -41,27 +56,55 @@ function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
41
56
  }
42
57
 
43
58
  /**
44
- * build渲染前解析数据组件(唯一的 await 边界)→ 树校验(与 text 宿主同一遍)→ 静态渲染
45
- * web 面;Selection 有挑选警告时在报告顶部前置一块警告 HTML;报告树里的 RunOverview
46
- * 已经渲染同一条时不重复。
59
+ * web 宿主的装载语义:选页 resolve(组合展开 + spec 取数,唯一的 await 边界)→
60
+ * 树校验(与 text 宿主同一遍)→ 静态渲染 web 面;Scope 有挑选警告时在报告顶部前置
61
+ * 一块警告 HTML(宿主是 warning 的唯一呈现者,组件数据不复制 warning)。
47
62
  */
48
63
  export async function renderReportToStaticHtml(
49
64
  definition: ReportDefinition,
50
- ctx: ReportContext,
65
+ ctx: ReportHostContext,
51
66
  options?: StaticHtmlOptions,
52
67
  ): Promise<string> {
53
- const node = await definition.build(ctx);
54
- const resolved = await resolveReportTree(node);
68
+ const page = pickReportPage(definition, options?.pageId);
69
+ const meta = buildReportMeta(definition, ctx.scope, page.id);
70
+ const resolved = await resolveReportTree(page.content, {
71
+ scope: ctx.scope,
72
+ results: ctx.results,
73
+ report: meta,
74
+ memo: new ResolveMemo(),
75
+ });
55
76
  validateReportTree(resolved);
56
77
  const webCtx: WebContext = {
57
78
  attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
58
79
  locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
59
80
  };
60
81
  const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
61
- const missingWarnings = ctx.selection.warnings.filter((warning) => {
62
- const escapedMessage = renderToStaticMarkup(React.createElement(React.Fragment, null, warning.message));
63
- return !body.includes(escapedMessage);
82
+ const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
83
+ return warnings + body;
84
+ }
85
+
86
+ /**
87
+ * 渲染一页报告树的 web 面(宿主逐页调用;页选择归宿主):resolve → validate → 静态渲染。
88
+ * Scope 有挑选警告时在页顶前置警告块(带 command 的警告渲染为可复制命令)——宿主是
89
+ * warning 的唯一呈现者,组件数据不复制 warning。ctx.report 是宿主规范化后的声明。
90
+ */
91
+ export async function renderReportTreeToStaticHtml(
92
+ tree: import("./tree.ts").ReportNode,
93
+ ctx: { scope: Scope; results: import("../results/types.ts").Results; report: import("./report.ts").ReportMeta },
94
+ options?: { attemptHref?: (locator: AttemptLocator) => string; locale?: ReportLocale },
95
+ ): Promise<string> {
96
+ const resolved = await resolveReportTree(tree, {
97
+ scope: ctx.scope,
98
+ results: ctx.results,
99
+ report: ctx.report,
100
+ memo: new ResolveMemo(),
64
101
  });
65
- const warnings = missingWarnings.length > 0 ? renderSelectionWarningsHtml(missingWarnings) : "";
102
+ validateReportTree(resolved);
103
+ const webCtx: WebContext = {
104
+ attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
105
+ locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
106
+ };
107
+ const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
108
+ const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
66
109
  return warnings + body;
67
110
  }
@@ -70,7 +70,7 @@ async function seedAttempt(
70
70
  }
71
71
 
72
72
  describe("loadAttemptEvidence", () => {
73
- it("四个 capability 全部具备:eval / execution / timing / diff", async () => {
73
+ it("四个 capability 全部具备:source / execution / timing / diff", async () => {
74
74
  const root = await makeRoot();
75
75
  const attempt = await seedAttempt(
76
76
  root,
@@ -90,10 +90,10 @@ describe("loadAttemptEvidence", () => {
90
90
  expect((actionNode as { span?: TraceSpan }).span).toBeDefined();
91
91
  expect(evidence.diff?.windows).toEqual(NONEMPTY_DIFF);
92
92
 
93
- expect(evidence.capabilities).toEqual({ eval: true, execution: true, timing: true, diff: true });
93
+ expect(evidence.capabilities).toEqual({ source: true, execution: true, timing: true, diff: true });
94
94
  });
95
95
 
96
- it("四个 capability 全部缺失:没有 source / events / trace / diff,不崩溃", async () => {
96
+ it("四个 capability 全部缺失:没有 sources / events / trace / diff,不崩溃", async () => {
97
97
  const root = await makeRoot();
98
98
  const attempt = await seedAttempt(root, { id: "weather/queens" });
99
99
 
@@ -102,7 +102,7 @@ describe("loadAttemptEvidence", () => {
102
102
  expect(evidence.evalSource).toBeNull();
103
103
  expect(evidence.execution).toBeNull();
104
104
  expect(evidence.diff).toBeNull();
105
- expect(evidence.capabilities).toEqual({ eval: false, execution: false, timing: false, diff: false });
105
+ expect(evidence.capabilities).toEqual({ source: false, execution: false, timing: false, diff: false });
106
106
  });
107
107
 
108
108
  it("有 events 没有 phases(旧 runner 产出):execution 为真、timing 为假", async () => {
@@ -12,7 +12,7 @@
12
12
  // "有什么可看"这件事产生同一个答案(没有),只是原因不同,原因留给 unavailable 文案区分,
13
13
  // 不留给 capability 位区分(capability 位只回答"要不要显示这个证据切面")。
14
14
  //
15
- // eval —— evalSource 非空 AND (至少有一行源码 或 至少有一条断言);理论上
15
+ // source —— evalSource 非空 AND (至少有一行源码 或 至少有一条断言);理论上
16
16
  // buildAnnotatedEvalSource 对空文件也会产出一行 ""(split("\n") 的自然结果),
17
17
  // 所以这条件实践中约等于"evalSource 非空",按文档口径原样写出两个子句,
18
18
  // 不依赖这个实现细节。
@@ -59,8 +59,8 @@ export interface EvidencePaths {
59
59
  * 对应的证据切面,不自己重新判断"这个 attempt 有没有 X"。
60
60
  */
61
61
  export interface AttemptEvidenceCapabilities {
62
- /** evalSource 非空且有内容(源码行或断言二选一非空) */
63
- eval: boolean;
62
+ /** evalSource 非空且有内容(源码行或断言二选一非空);对应 CLI 的 `--source` 证据切面。 */
63
+ source: boolean;
64
64
  /** events 非空数组——有骨架可看。 */
65
65
  execution: boolean;
66
66
  /** result.json 带 phases(阶段计时)——`--timing` 时间树可渲染。 */
@@ -79,7 +79,7 @@ export interface AttemptEvidence {
79
79
  * 的调用方直接读结构化字段(如按 experimentId 分组、按时间排序)。 */
80
80
  identity: AttemptIdentity;
81
81
  result: EvalResult;
82
- /** 运行时保存的 Eval 源码标注(`--eval` 切面);没有 sources() 就是 null,不伪造空文档。 */
82
+ /** 运行时保存的 Eval 源码标注(`--source` 切面);没有 sources() 就是 null,不伪造空文档。 */
83
83
  evalSource: AnnotatedEvalSource | null;
84
84
  /** 标准事件流 + OTel enrichment(`--execution` 切面);没有非空 events 就是 null,
85
85
  * 不产出一棵只剩 telemetry-only 节点、没有真实骨架的误导性树(见头注 execution 门槛)。 */
@@ -144,7 +144,7 @@ export async function loadAttemptEvidence(attempt: AttemptHandle): Promise<Attem
144
144
  trace,
145
145
  artifactPaths: { dir: attemptArtifactDir(attempt) },
146
146
  capabilities: {
147
- eval: evalCapable,
147
+ source: evalCapable,
148
148
  execution: execution !== null,
149
149
  timing: (attempt.result.phases?.length ?? 0) > 0,
150
150
  diff: diffCapable,
@@ -24,7 +24,7 @@ import {
24
24
  redactSpans,
25
25
  type Redactor,
26
26
  } from "./publish.ts";
27
- import type { ArtifactKind, AttemptHandle, Selection, Snapshot, SnapshotMeta } from "./types.ts";
27
+ import type { ArtifactKind, AttemptHandle, Scope, Snapshot, SnapshotMeta } from "./types.ts";
28
28
 
29
29
  /** 缺省携带的 artifact:events / trace / o11y / agentSetup / sources;diff 不截断、可达百 MB,缺省不带。 */
30
30
  const DEFAULT_PUBLISH_ARTIFACTS: ArtifactKind[] = ["events", "trace", "o11y", "agentSetup", "sources"];
@@ -50,15 +50,15 @@ export interface CopySnapshotsResult {
50
50
 
51
51
  /**
52
52
  * 把选中快照复制成 `destDir` 下的一个标准结果根目录(`<experiment-dir>/<源快照目录名>/`,
53
- * 快照目录名原样保留,身份不变)。输入收 Selection 或手工挑的 Snapshot[]
53
+ * 快照目录名原样保留,身份不变)。输入收 Scope 或手工挑的 Snapshot[]
54
54
  * (与 Reports 计算函数同一输入约定)。目标目录非空即报错,不静默覆盖、不合并。
55
55
  */
56
56
  export async function copySnapshots(
57
- selection: Selection | Snapshot[],
57
+ scope: Scope | readonly Snapshot[],
58
58
  destDir: string,
59
59
  opts: CopySnapshotsOptions,
60
60
  ): Promise<CopySnapshotsResult> {
61
- const selected = Array.isArray(selection) ? selection : selection.snapshots;
61
+ const selected = Array.isArray(scope) ? (scope as readonly Snapshot[]) : (scope as Scope).snapshots;
62
62
  if (selected.length === 0) {
63
63
  throw new Error(
64
64
  "copySnapshots got no snapshots to copy. Check the experiments filter, or pass snapshots from openResults().latest().",
@@ -92,7 +92,7 @@ export async function copySnapshots(
92
92
  continue;
93
93
  }
94
94
  warnings.push(
95
- `warning: multiple snapshots selected for experiment "${snapshot.experimentId}"; kept the newest one, dropped the rest. Dedupe with Selection.filter() or pick a single snapshot per experiment before copySnapshots to avoid this.`,
95
+ `warning: multiple snapshots selected for experiment "${snapshot.experimentId}"; kept the newest one, dropped the rest. Dedupe with Scope.filter() or pick a single snapshot per experiment before copySnapshots to avoid this.`,
96
96
  );
97
97
  if (isNewerSnapshot(snapshot, existing)) byExperiment.set(snapshot.experimentId, snapshot);
98
98
  }
@@ -101,7 +101,7 @@ export async function copySnapshots(
101
101
  // 超过 PUBLISH_FILE_MAX_BYTES 就整体失败,不留半成品目标目录。
102
102
  const planned: PlannedFile[] = [];
103
103
  for (const snapshot of byExperiment.values()) {
104
- planned.push(...(await planOneSnapshot(snapshot, selected, dest, kinds, redactor)));
104
+ planned.push(...(await planOneSnapshot(snapshot, [...selected], dest, kinds, redactor)));
105
105
  }
106
106
  const oversized = planned.filter((f) => f.bytes.byteLength > PUBLISH_FILE_MAX_BYTES);
107
107
  if (oversized.length > 0) {
@@ -24,7 +24,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
24
24
  import { tmpdir } from "node:os";
25
25
  import { join } from "node:path";
26
26
  import { openResults } from "./index.ts";
27
- import type { Selection, SelectionWarning } from "./index.ts";
27
+ import type { Scope, ScopeWarning } from "./index.ts";
28
28
  import { selectCurrentResults, type ResultScope } from "./select.ts";
29
29
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
30
30
  import { runShow, type ShowFlags } from "../show/index.ts";
@@ -132,7 +132,7 @@ interface NormSelection {
132
132
  experiments: NormExperiment[];
133
133
  }
134
134
 
135
- function normalizeWarning(w: SelectionWarning): NormWarning {
135
+ function normalizeWarning(w: ScopeWarning): NormWarning {
136
136
  switch (w.kind) {
137
137
  case "partial-coverage":
138
138
  return { kind: w.kind, experimentId: w.experimentId, covered: w.covered, total: w.total };
@@ -144,7 +144,7 @@ function normalizeWarning(w: SelectionWarning): NormWarning {
144
144
  }
145
145
  }
146
146
 
147
- function normalizeSelection(selection: Selection): NormSelection {
147
+ function normalizeSelection(selection: Scope): NormSelection {
148
148
  return {
149
149
  warnings: selection.warnings.map(normalizeWarning),
150
150
  experiments: selection.snapshots.map((snapshot) => ({
@@ -167,7 +167,7 @@ function normalizeSelection(selection: Selection): NormSelection {
167
167
  async function showText(root: string, patterns: string[], flags: ShowFlags = {}): Promise<string> {
168
168
  let out = "";
169
169
  let err = "";
170
- const code = await runShow(root, patterns, { run: root, ...flags }, {
170
+ const code = await runShow(root, patterns, { results: root, ...flags }, {
171
171
  out: (s) => (out += s),
172
172
  err: (s) => (err += s),
173
173
  width: 120,
@@ -179,8 +179,8 @@ async function showText(root: string, patterns: string[], flags: ShowFlags = {})
179
179
 
180
180
  /** view 的 web 面:loadViewScan → 报告槽 HTML(en)。 */
181
181
  async function viewHtml(root: string, opts: ViewScanOptions = {}): Promise<string> {
182
- const { reportHtml } = await loadViewScan(root, opts);
183
- return reportHtml.en;
182
+ const { reportPages } = await loadViewScan(root, opts);
183
+ return reportPages.map((page) => page.html.en).join("\n");
184
184
  }
185
185
 
186
186
  /** 两个宿主构造给选择器的 scope 完全同形:验证读源无误,避免"我以为它们一样"。 */
@@ -453,20 +453,32 @@ describe("宿主接线 · show text 面与 view web 面反映同一批事实", (
453
453
  await writeFile(
454
454
  reportPath,
455
455
  [
456
+ "// 手搭新 defineReport 产物(kind + brand + 规范化页列表);组合组件从 ctx.scope 读注入范围。",
456
457
  'const FACES = Symbol.for("niceeval.report.faces");',
457
- "const Echo = (props) => Echo[FACES].web(props);",
458
- "Echo[FACES] = {",
458
+ 'const COMPOSE = Symbol.for("niceeval.report.compose");',
459
+ "const Leaf = (props) => Leaf[FACES].web(props);",
460
+ "Leaf[FACES] = {",
459
461
  ' web: (props) => "EVALS[" + props.ids + "]",',
460
462
  ' text: (props) => "EVALS[" + props.ids + "]",',
461
463
  "};",
464
+ "const Echo = () => { throw new Error(\"compose only\"); };",
465
+ "Echo[COMPOSE] = (_props, ctx) => ({",
466
+ ' $$typeof: Symbol.for("react.transitional.element"),',
467
+ " type: Leaf,",
468
+ ' props: { ids: ctx.scope.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(",") },',
469
+ " key: null,",
470
+ "});",
462
471
  "export default {",
472
+ ' kind: "report",',
463
473
  ' [Symbol.for("niceeval.report.definition")]: true,',
464
- " build: (ctx) => ({",
465
- ' $$typeof: Symbol.for("react.transitional.element"),',
466
- " type: Echo,",
467
- ' props: { ids: ctx.selection.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(",") },',
468
- " key: null,",
469
- " }),",
474
+ " links: [],",
475
+ " scripts: [],",
476
+ " styles: [],",
477
+ " pages: [{",
478
+ ' id: "report",',
479
+ ' title: "Report",',
480
+ ' content: { $$typeof: Symbol.for("react.transitional.element"), type: Echo, props: {}, key: null },',
481
+ " }],",
470
482
  "};",
471
483
  "",
472
484
  ].join("\n"),
@@ -1,6 +1,6 @@
1
1
  // niceeval/results —— 实验结果数据的读写库(定稿见 docs/feature/results/library.md、docs/feature/results/architecture.md)。
2
2
  //
3
- // 读:openResults(实验 → 快照 → eval → attempt 分层、skipped、latest() Selection);
3
+ // 读:openResults(实验 → 快照 → eval → attempt 分层、skipped、latest() / current() Scope);
4
4
  // 写:createResultsWriter(快照声明 + attempt 增量落盘 + finish 补 completedAt);
5
5
  // 发布:copySnapshots(格式感知复制 + knownEvalIds 补记);
6
6
  // 身份:dedupeAttempts(跨快照聚合前按 (experimentId, evalId, attempt, startedAt) 去重)、isNewerSnapshot。
@@ -44,7 +44,13 @@ export {
44
44
  } from "./attempt-evidence.ts";
45
45
  export { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
46
46
  export { groupIncompatibleVersionSkips, type SkippedVersionGroup } from "./skipped-notice.ts";
47
- export { dedupeAttempts, isNewerSnapshot } from "./select.ts";
47
+ export {
48
+ comparabilityConfigOf,
49
+ dedupeAttempts,
50
+ deepEqualJson,
51
+ isNewerSnapshot,
52
+ type ComparabilityConfig,
53
+ } from "./select.ts";
48
54
  export { copySnapshots, type CopySnapshotsOptions, type CopySnapshotsResult } from "./copy.ts";
49
55
  export {
50
56
  createResultsWriter,
@@ -73,8 +79,8 @@ export {
73
79
  type Eval,
74
80
  type Experiment,
75
81
  type Results,
76
- type Selection,
77
- type SelectionWarning,
82
+ type Scope,
83
+ type ScopeWarning,
78
84
  type SkippedDir,
79
85
  type Snapshot,
80
86
  type SnapshotMeta,
@@ -13,7 +13,7 @@ import type { O11ySummary, StreamEvent, TraceSpan } from "../types.ts";
13
13
  import type { AgentSetupManifest, DiffData, SourceArtifact } from "../types.ts";
14
14
  import { deriveDiffData } from "../scoring/diff.ts";
15
15
  import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, classifySnapshot } from "./format.ts";
16
- import { isNewerSnapshot, selectLatest } from "./select.ts";
16
+ import { isNewerSnapshot, selectCurrentResults, selectLatest } from "./select.ts";
17
17
  import {
18
18
  encodeAttemptLocator,
19
19
  resolveAttemptLocator,
@@ -27,7 +27,7 @@ import type {
27
27
  Eval,
28
28
  Experiment,
29
29
  Results,
30
- Selection,
30
+ Scope,
31
31
  SkippedDir,
32
32
  Snapshot,
33
33
  SnapshotMeta,
@@ -171,13 +171,17 @@ export async function openResults(dir: string): Promise<Results> {
171
171
  }
172
172
 
173
173
  function makeResults(experiments: Experiment[], skipped: SkippedDir[]): Results {
174
- return {
174
+ const results: Results = {
175
175
  experiments,
176
176
  skipped,
177
- latest(opts?: { experiments?: string | string[] }): Selection {
177
+ latest(opts?: { experiments?: string | string[] }): Scope {
178
178
  return selectLatest(experiments, opts);
179
179
  },
180
+ current(opts?: { experiments?: string | string[] }): Scope {
181
+ return selectCurrentResults(results, { experiment: opts?.experiments });
182
+ },
180
183
  };
184
+ return results;
181
185
  }
182
186
 
183
187
  // ───────────────────────── 目录扫描 ─────────────────────────
@@ -343,7 +343,7 @@ describe("results.latest() · Selection", () => {
343
343
  const stale = latest.warnings.filter((w) => w.kind === "stale-snapshot");
344
344
  expect(stale).toHaveLength(1);
345
345
  expect(stale[0]).toMatchObject({ experimentId: "mid/a", startedAt: "2026-07-01T08:00:00.000Z", latestStartedAt: "2026-07-05T08:00:00.000Z" });
346
- expect(stale[0].message).toContain("predates the latest run in this selection by 4 days");
346
+ expect(stale[0].message).toContain("predates the latest run in this scope by 4 days");
347
347
  });
348
348
 
349
349
  it("unfinished-snapshot:选中快照缺 completedAt", async () => {
@@ -976,8 +976,9 @@ describe("AttemptLocator · 落盘 / 读取 / 携带 / 撞车", () => {
976
976
  const handMadeResults: Results = {
977
977
  experiments: [{ id: "e", snapshots: [snapshot], latest: snapshot, evalIds: ["q1"] }],
978
978
  skipped: [],
979
- // filter() 本测试不调用,用不到,给个占位实现即可满足 Selection 接口。
980
- latest: () => ({ snapshots: [snapshot], warnings: [], filter: () => { throw new Error("not implemented"); } }),
979
+ // filter() 本测试不调用,用不到,给个占位实现即可满足 Scope 接口。
980
+ latest: () => ({ mode: "latest-snapshots" as const, snapshots: [snapshot], attempts: snapshot.attempts, warnings: [], filter: () => { throw new Error("not implemented"); } }),
981
+ current: () => { throw new Error("not implemented"); },
981
982
  };
982
983
  // 这份 locator 语法合法、甚至真的对应 handMadeResults 里那个 attempt 的身份,
983
984
  // 但 handMadeResults 没经过 openResults(),locatorIndexByResults 里查不到它 —— 空索引,not-found。