niceeval 0.8.1 → 0.9.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 (140) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +3 -1
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +69 -2
  10. package/dist/report/components.js +152 -3
  11. package/dist/report/compute.d.ts +28 -1
  12. package/dist/report/compute.js +123 -0
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +68 -55
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.d.ts +1 -1
  38. package/dist/report/tree.js +7 -2
  39. package/dist/report/types.d.ts +45 -0
  40. package/dist/report/web.d.ts +5 -4
  41. package/dist/report/web.js +7 -22
  42. package/dist/results/select.d.ts +17 -4
  43. package/dist/results/select.js +76 -11
  44. package/dist/results/types.d.ts +26 -0
  45. package/dist/runner/fingerprint.d.ts +3 -3
  46. package/dist/runner/sandbox-selection.d.ts +12 -0
  47. package/dist/runner/types.d.ts +18 -6
  48. package/dist/sandbox/types.d.ts +12 -0
  49. package/dist/shared/aggregate.d.ts +1 -1
  50. package/dist/shared/aggregate.js +1 -1
  51. package/docs-site/zh/explanation/evals.mdx +2 -1
  52. package/docs-site/zh/explanation/experiment.mdx +2 -0
  53. package/docs-site/zh/how-to/custom-reports.mdx +23 -7
  54. package/docs-site/zh/how-to/experiments.mdx +2 -2
  55. package/docs-site/zh/how-to/publish-report.mdx +11 -4
  56. package/docs-site/zh/how-to/viewing-results.mdx +17 -8
  57. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  58. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  59. package/docs-site/zh/reference/cli.mdx +3 -3
  60. package/docs-site/zh/reference/define-eval.mdx +8 -0
  61. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  62. package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
  63. package/package.json +2 -1
  64. package/src/agents/bub.ts +13 -1
  65. package/src/agents/claude-code.test.ts +43 -1
  66. package/src/agents/claude-code.ts +32 -14
  67. package/src/agents/codex.test.ts +168 -1
  68. package/src/agents/codex.ts +51 -15
  69. package/src/agents/mcp.ts +31 -0
  70. package/src/agents/post-setup.ts +33 -0
  71. package/src/agents/types.ts +28 -7
  72. package/src/cli.ts +15 -14
  73. package/src/define.ts +3 -0
  74. package/src/i18n/en.ts +8 -5
  75. package/src/i18n/zh-CN.ts +8 -4
  76. package/src/index.ts +1 -0
  77. package/src/report/built-in/index.tsx +8 -7
  78. package/src/report/built-in/standard.tsx +59 -0
  79. package/src/report/components.tsx +218 -2
  80. package/src/report/compute.ts +138 -1
  81. package/src/report/dual-render.test.tsx +141 -14
  82. package/src/report/index.ts +20 -0
  83. package/src/report/locale.ts +83 -1
  84. package/src/report/react/AttemptList.tsx +13 -1
  85. package/src/report/react/CopyFixPrompt.tsx +37 -0
  86. package/src/report/react/HeroCard.tsx +59 -0
  87. package/src/report/react/PoweredBy.tsx +20 -0
  88. package/src/report/react/ScopeWarnings.tsx +74 -0
  89. package/src/report/react/TraceWaterfall.tsx +78 -0
  90. package/src/report/react/enhance.js +14 -0
  91. package/src/report/react/index.tsx +11 -0
  92. package/src/report/react/styles.css +193 -7
  93. package/src/report/report.ts +99 -64
  94. package/src/report/scope-warnings.ts +155 -0
  95. package/src/report/site-components.test.tsx +526 -0
  96. package/src/report/text/faces.ts +66 -0
  97. package/src/report/tree.ts +10 -3
  98. package/src/report/types.ts +51 -0
  99. package/src/report/web.ts +7 -40
  100. package/src/results/host-equivalence.test.ts +6 -2
  101. package/src/results/open.ts +5 -4
  102. package/src/results/results.test.ts +78 -1
  103. package/src/results/select.ts +80 -12
  104. package/src/results/types.ts +27 -0
  105. package/src/runner/attempt.ts +10 -9
  106. package/src/runner/discover.test.ts +9 -1
  107. package/src/runner/discover.ts +3 -3
  108. package/src/runner/fingerprint.ts +9 -4
  109. package/src/runner/ledger.test.ts +30 -1
  110. package/src/runner/ledger.ts +26 -4
  111. package/src/runner/run.ts +5 -1
  112. package/src/runner/sandbox-selection.test.ts +131 -0
  113. package/src/runner/sandbox-selection.ts +110 -0
  114. package/src/runner/types.ts +19 -2
  115. package/src/sandbox/types.ts +6 -0
  116. package/src/shared/aggregate.ts +1 -1
  117. package/src/show/index.ts +19 -12
  118. package/src/show/render.ts +12 -12
  119. package/src/show/report-host.test.ts +34 -17
  120. package/src/show/report-host.ts +6 -5
  121. package/src/show/show.test.ts +141 -4
  122. package/src/view/app/App.test.tsx +79 -17
  123. package/src/view/app/App.tsx +25 -74
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/client-dist/app.css +1 -1
  130. package/src/view/client-dist/app.js +20 -20
  131. package/src/view/data.ts +21 -10
  132. package/src/view/index.ts +2 -12
  133. package/src/view/server.ts +4 -4
  134. package/src/view/shared/types.ts +11 -6
  135. package/src/view/styles.css +9 -252
  136. package/src/view/view-report.test.ts +99 -33
  137. package/src/view/app/components/LazyArtifact.tsx +0 -51
  138. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  139. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  140. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -1,26 +1,15 @@
1
1
  // web 宿主(view --report)的装载入口:同一棵树走 web 面,renderToStaticMarkup 吐静态
2
2
  // HTML 烘进查看器的报告槽。只有这一侧真正 import react-dom(import 边界即运行时边界),
3
3
  // 所以本文件不从 niceeval/report 的入口 re-export —— 宿主与测试按源路径 import。
4
- import * as React from "react";
5
4
  import { renderToStaticMarkup } from "react-dom/server";
6
5
  import { resolveReportTree, runWithWebContext, validateReportTree, ResolveMemo, } from "./tree.js";
7
6
  import { DEFAULT_REPORT_LOCALE } from "./locale.js";
8
7
  import { buildReportMeta, pickReportPage } from "./report.js";
9
- /**
10
- * 挑选警告的 HTML 形态:宿主级前置块(`.nre nre-report-warnings` 外壳内一个
11
- * `ul.nre-warnings` + `li.nre-warning[data-kind]`,复用 styles.css 已有样式)。
12
- * 带 `command` 的警告把命令渲染为可复制块(`.nre-warning-command`);无 command 的
13
- * 只显示 message,不硬造动作。经 renderToStaticMarkup 走 React,文本自动转义。
14
- */
15
- function renderScopeWarningsHtml(scope) {
16
- return renderToStaticMarkup(React.createElement("div", { className: "nre nre-report-warnings" }, React.createElement("ul", { className: "nre-warnings" }, scope.warnings.map((w, i) => React.createElement("li", { key: i, className: "nre-warning", "data-kind": w.kind }, w.message, "command" in w && w.command
17
- ? React.createElement("code", { className: "nre-warning-command", "data-nre-copy": w.command }, w.command)
18
- : null)))));
19
- }
20
8
  /**
21
9
  * web 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→
22
- * 树校验(与 text 宿主同一遍)→ 静态渲染 web 面;Scope 有挑选警告时在报告顶部前置
23
- * 一块警告 HTML(宿主是 warning 的唯一呈现者,组件数据不复制 warning)。
10
+ * 树校验(与 text 宿主同一遍)→ 静态渲染 web 面。宿主不在报告树外另设警告通道——
11
+ * 挑选警告的呈现件是 `ScopeWarnings` 组件,内建报告每页都放它,自定义报告放不放是
12
+ * 作者义务(docs/feature/reports/architecture.md「Scope 是计算入口」)。
24
13
  */
25
14
  export async function renderReportToStaticHtml(definition, ctx, options) {
26
15
  const page = pickReportPage(definition, options?.pageId);
@@ -36,14 +25,12 @@ export async function renderReportToStaticHtml(definition, ctx, options) {
36
25
  attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
37
26
  locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
38
27
  };
39
- const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved));
40
- const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
41
- return warnings + body;
28
+ return runWithWebContext(webCtx, () => renderToStaticMarkup(resolved));
42
29
  }
43
30
  /**
44
31
  * 渲染一页报告树的 web 面(宿主逐页调用;页选择归宿主):resolve → validate → 静态渲染。
45
- * Scope 有挑选警告时在页顶前置警告块(带 command 的警告渲染为可复制命令)——宿主是
46
- * warning 的唯一呈现者,组件数据不复制 warning。ctx.report 是宿主规范化后的声明。
32
+ * 挑选警告由页内的 `ScopeWarnings` 组件呈现,宿主不前置任何树外块。
33
+ * ctx.report 是宿主规范化后的声明。
47
34
  */
48
35
  export async function renderReportTreeToStaticHtml(tree, ctx, options) {
49
36
  const resolved = await resolveReportTree(tree, {
@@ -57,7 +44,5 @@ export async function renderReportTreeToStaticHtml(tree, ctx, options) {
57
44
  attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
58
45
  locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
59
46
  };
60
- const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved));
61
- const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
62
- return warnings + body;
47
+ return runWithWebContext(webCtx, () => renderToStaticMarkup(resolved));
63
48
  }
@@ -1,12 +1,17 @@
1
1
  import type { AttemptHandle, DedupeWarning, Experiment, Results, Scope, ScopeWarning, Snapshot } from "./types.ts";
2
2
  import type { ExperimentRunInfo, JsonValue } from "../types.ts";
3
- /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
4
- export declare function selectLatest(experiments: Experiment[], opts?: {
3
+ /**
4
+ * Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。
5
+ * 收整个 `Results` 而不是裸 `Experiment[]`,是为了同时取 `skipped` / `root` 生成
6
+ * `unreadable-snapshot` 警告(非实验作用域,不受 `opts.experiments` 过滤 —— 那些落盘
7
+ * 本来就没能解析出 experimentId,没有前缀可过滤)。
8
+ */
9
+ export declare function selectLatest(results: Pick<Results, "experiments" | "skipped" | "root">, opts?: {
5
10
  experiments?: string | string[];
6
11
  }): Scope;
7
12
  /** selectCurrentResults 的范围输入:experiment id 前缀与 eval id 前缀,都可缺省。 */
8
13
  export interface ResultScope {
9
- /** experiment id 前缀(--experiment),分段匹配语义同 filterExperiments。 */
14
+ /** experiment id 前缀(--exp),分段匹配语义同 filterExperiments。 */
10
15
  experiment?: string | string[];
11
16
  /** eval id 前缀(位置参数),收窄 Scope 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
12
17
  patterns?: string[];
@@ -63,5 +68,13 @@ export declare function dedupeAttempts(attempts: AttemptHandle[]): {
63
68
  };
64
69
  /** 快照新旧比较:startedAt 优先,同刻按快照目录名(时间戳 + 随机后缀,字典序即时序)。 */
65
70
  export declare function isNewerSnapshot(a: Snapshot, b: Snapshot): boolean;
66
- /** experiment id 分段前缀过滤(--experiment / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
71
+ /** experiment id 分段前缀过滤(--exp / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
67
72
  export declare function filterExperiments(experiments: Experiment[], filter?: string | string[]): Experiment[];
73
+ /**
74
+ * stale 警告的人话时距:选粒度最大的单位,四舍五入。结构化形态是单源——message 的英文时距
75
+ * 与 ScopeWarnings 徽标的本地化时距都从这里出,阈值不写两份。
76
+ */
77
+ export declare function gapParts(fromIso: string, toIso: string): {
78
+ n: number;
79
+ unit: "second" | "minute" | "hour" | "day";
80
+ };
@@ -4,9 +4,14 @@
4
4
  // 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Scope 走 ——
5
5
  // 渲染与否在消费方(message 是渲染好的英文句子,以下一步收尾),但缺口不静默。
6
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);
7
+ /**
8
+ * Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。
9
+ * 收整个 `Results` 而不是裸 `Experiment[]`,是为了同时取 `skipped` / `root` 生成
10
+ * `unreadable-snapshot` 警告(非实验作用域,不受 `opts.experiments` 过滤 —— 那些落盘
11
+ * 本来就没能解析出 experimentId,没有前缀可过滤)。
12
+ */
13
+ export function selectLatest(results, opts) {
14
+ const selected = filterExperiments(results.experiments, opts?.experiments);
10
15
  const snapshots = selected.map((exp) => exp.latest);
11
16
  const warnings = [];
12
17
  // stale 的基准:Scope 中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)。
@@ -52,6 +57,7 @@ export function selectLatest(experiments, opts) {
52
57
  });
53
58
  }
54
59
  }
60
+ warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
55
61
  return makeScope("latest-snapshots", snapshots, warnings);
56
62
  }
57
63
  /** 一个快照的可比性配置投影;pairsByFlag 与 experimentListData 复用同一字段集。 */
@@ -188,8 +194,61 @@ export function selectCurrentResults(results, scope = {}) {
188
194
  });
189
195
  }
190
196
  }
197
+ warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
191
198
  return makeScope("current-evals", snapshots, warnings);
192
199
  }
200
+ /**
201
+ * `results.skipped` 里每一条不可读落盘 → 一条 `unreadable-snapshot` ScopeWarning。
202
+ * 非实验作用域(没有 experimentId 字段):`latest()` / `current()` 都原样带上全部
203
+ * `skipped` 条目,不受 `opts.experiments` 前缀过滤影响(那些落盘本来就没能解析出
204
+ * experimentId,没有前缀可比);`makeScope().filter()` 按「非实验作用域的警告保留」
205
+ * 规则自动放行,不需要额外分支。
206
+ */
207
+ function unreadableSnapshotWarnings(skipped, root) {
208
+ return skipped.map((s) => {
209
+ switch (s.reason) {
210
+ case "incompatible-version": {
211
+ const producer = s.producer;
212
+ const schemaText = s.schemaVersion !== undefined ? ` (schemaVersion ${s.schemaVersion})` : "";
213
+ if (producer?.name === "niceeval" && producer.version) {
214
+ const command = `npx niceeval@${producer.version} show --results ${root}`;
215
+ return {
216
+ kind: "unreadable-snapshot",
217
+ dir: s.dir,
218
+ reason: s.reason,
219
+ message: `snapshot at "${s.dir}" was written by niceeval ${producer.version}${schemaText} and cannot be read by this version; run \`${command}\` to open it`,
220
+ command,
221
+ };
222
+ }
223
+ const writtenBy = producer?.name
224
+ ? `${producer.name}${producer.version ? ` ${producer.version}` : ""}`
225
+ : "an incompatible tool version";
226
+ return {
227
+ kind: "unreadable-snapshot",
228
+ dir: s.dir,
229
+ reason: s.reason,
230
+ message: `snapshot at "${s.dir}" was written by ${writtenBy}${schemaText} and cannot be read by this version; open it with the tool version that produced it`,
231
+ };
232
+ }
233
+ case "malformed": {
234
+ const detail = s.detail ? ` (${s.detail})` : "";
235
+ return {
236
+ kind: "unreadable-snapshot",
237
+ dir: s.dir,
238
+ reason: s.reason,
239
+ message: `snapshot at "${s.dir}" is malformed${detail} and was skipped; inspect snapshot.json in that directory for corrupted JSON or a missing required field`,
240
+ };
241
+ }
242
+ case "incomplete":
243
+ return {
244
+ kind: "unreadable-snapshot",
245
+ dir: s.dir,
246
+ reason: s.reason,
247
+ message: `snapshot at "${s.dir}" has attempt data but no snapshot.json (likely interrupted before metadata was written) and was skipped; inspect ${s.dir} — completed attempts remain on disk for manual review`,
248
+ };
249
+ }
250
+ });
251
+ }
193
252
  /**
194
253
  * Scope 构造:attempts 按口径物化(快照 attempts 的平铺);filter 只删不换 —— 快照删减,
195
254
  * attempts 随之同步修剪,warnings 修剪规则是「experimentId 不在幸存快照中的丢弃,
@@ -253,7 +312,7 @@ export function isNewerSnapshot(a, b) {
253
312
  return byStart > 0;
254
313
  return a.dir.localeCompare(b.dir) > 0;
255
314
  }
256
- /** experiment id 分段前缀过滤(--experiment / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
315
+ /** experiment id 分段前缀过滤(--exp / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
257
316
  export function filterExperiments(experiments, filter) {
258
317
  if (filter === undefined)
259
318
  return experiments;
@@ -261,18 +320,24 @@ export function filterExperiments(experiments, filter) {
261
320
  const prefixes = (Array.isArray(filter) ? filter : [filter]).map((p) => p.replace(/\/+$/, ""));
262
321
  return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
263
322
  }
264
- /** stale 警告的人话时距:选粒度最大的单位,四舍五入。 */
265
- function humanizeGap(fromIso, toIso) {
323
+ /**
324
+ * stale 警告的人话时距:选粒度最大的单位,四舍五入。结构化形态是单源——message 的英文时距
325
+ * 与 ScopeWarnings 徽标的本地化时距都从这里出,阈值不写两份。
326
+ */
327
+ export function gapParts(fromIso, toIso) {
266
328
  const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
267
329
  const seconds = Math.round(ms / 1000);
268
330
  if (seconds < 90)
269
- return `${seconds} second${seconds === 1 ? "" : "s"}`;
331
+ return { n: seconds, unit: "second" };
270
332
  const minutes = Math.round(seconds / 60);
271
333
  if (minutes < 90)
272
- return `${minutes} minute${minutes === 1 ? "" : "s"}`;
334
+ return { n: minutes, unit: "minute" };
273
335
  const hours = Math.round(minutes / 60);
274
336
  if (hours < 36)
275
- return `${hours} hour${hours === 1 ? "" : "s"}`;
276
- const days = Math.round(hours / 24);
277
- return `${days} day${days === 1 ? "" : "s"}`;
337
+ return { n: hours, unit: "hour" };
338
+ return { n: Math.round(hours / 24), unit: "day" };
339
+ }
340
+ function humanizeGap(fromIso, toIso) {
341
+ const { n, unit } = gapParts(fromIso, toIso);
342
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
278
343
  }
@@ -142,6 +142,12 @@ export interface SkippedDir {
142
142
  }
143
143
  /** openResults 的返回:experiments 分层;skipped 不静默丢。 */
144
144
  export interface Results {
145
+ /**
146
+ * 结果根目录的绝对路径(`openResults()` 入参解析后的原样值,不论传入的是结果根、
147
+ * 实验目录、快照目录还是某个 snapshot.json)。`unreadable-snapshot` 警告拼版本化
148
+ * `command`(`npx niceeval@<version> show --results <root>`)时取它。
149
+ */
150
+ root: string;
145
151
  /** 每个实验一项,挂着自己的全部历史(id 字典序)。 */
146
152
  experiments: Experiment[];
147
153
  skipped: SkippedDir[];
@@ -218,6 +224,26 @@ export type ScopeWarning = {
218
224
  message: string;
219
225
  /** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
220
226
  command: string;
227
+ } | {
228
+ /**
229
+ * 扫描结果根遇到的不可读快照:schema 不兼容、JSON 损坏 / 必需字段错误(malformed)、
230
+ * attempt 已写入但缺 `snapshot.json`(incomplete)。该快照被跳过,不挡其余结果
231
+ * (非 niceeval JSON 静默忽略,不产生这个 kind)。非实验作用域(没有 experimentId
232
+ * 字段) —— `Scope.filter()` 修剪时恒保留。
233
+ */
234
+ kind: "unreadable-snapshot";
235
+ /** 该快照目录的绝对路径。 */
236
+ dir: string;
237
+ /** 与 `SkippedDir.reason` 同一取值集,原样透传。 */
238
+ reason: "incompatible-version" | "malformed" | "incomplete";
239
+ message: string;
240
+ /**
241
+ * 只有 reason 为 `incompatible-version` 且能确定是 niceeval 自己产出(`producer.name
242
+ * === "niceeval"` 且带 `producer.version`)时给出:`npx niceeval@<version> show --results
243
+ * <root>`。第三方 producer、版本信息缺失,或 reason 为 malformed / incomplete 时省略——
244
+ * 这些情况没有单条命令能解决,message 改给定位动作。
245
+ */
246
+ command?: string;
221
247
  };
222
248
  /** dedupeAttempts 的警告:身份键缺 startedAt,宁可不去重也不误删。 */
223
249
  export interface DedupeWarning {
@@ -1,11 +1,11 @@
1
- import type { DiscoveredEval, EvalResult } from "../types.ts";
1
+ import type { DiscoveredEval, EvalResult, SandboxOption } from "../types.ts";
2
2
  import type { AgentRun } from "./types.ts";
3
3
  export declare function cacheKey(run: AgentRun, evalId: string): string;
4
4
  /**
5
5
  * @param sourceCache 按 sourcePath 缓存文件内容:一个矩阵(实验 × eval)会对同一批源文件
6
6
  * 反复算指纹,不带缓存会在任何 attempt 起跑前做 E×N 次重复文件读。
7
7
  */
8
- export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>): Promise<string>;
8
+ export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>, configSandbox?: SandboxOption): Promise<string>;
9
9
  export interface CarryPlan {
10
10
  /** `cacheKey(run, evalId)` → 本次规划出的指纹,供调用方按同一口径判断"这条要不要携入"。 */
11
11
  plannedFingerprints: Map<string, string>;
@@ -20,4 +20,4 @@ export interface CarryPlan {
20
20
  * 不一致,live 表格就会显示"还在等名额",而 run.ts 其实已经把它筛掉、根本不会调度这个 attempt
21
21
  * (见 memory 的 live-carry-row-shows-waiting-forever)。
22
22
  */
23
- export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined): Promise<CarryPlan>;
23
+ export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined, configSandbox?: SandboxOption): Promise<CarryPlan>;
@@ -0,0 +1,12 @@
1
+ import type { DiscoveredEval, SandboxOption, SandboxRunInfo } from "../types.ts";
2
+ import type { AgentRun } from "./types.ts";
3
+ /** 该 eval 实际起步的 spec:未声明 environment 用基础 spec;声明了则查表派生并缓存。 */
4
+ export declare function sandboxForEval(run: AgentRun, evalDef: DiscoveredEval, fallback?: SandboxOption): SandboxOption | undefined;
5
+ /** 在 dry-run / carry / concurrency / attempt 展开之前一次性查表;全部缺项一次穷举,不等到花费发生后才出现。 */
6
+ export declare function prepareRunSandboxes(evals: DiscoveredEval[], runs: AgentRun[], fallback?: SandboxOption): void;
7
+ /** ExperimentRunInfo 的 sandbox 投影:顶层恒为基础 spec;sandboxByEval 只含声明了 environment 的选中 eval。 */
8
+ export declare function sandboxProjection(run: AgentRun, fallback?: SandboxOption): {
9
+ sandbox?: SandboxRunInfo;
10
+ sandboxByEval?: Record<string, SandboxRunInfo>;
11
+ };
12
+ export declare function resolvedSandboxRecommendedConcurrency(evals: DiscoveredEval[], runs: AgentRun[], fallback?: SandboxOption): number;
@@ -25,11 +25,14 @@ export interface ExperimentRunInfo {
25
25
  /** evals 过滤器的指纹(数组内容 / 函数体哈希),供「配置没变」判断;与 selectedEvalIds 一起取代原过滤器。 */
26
26
  evalFilterFingerprint?: string;
27
27
  /** provider 名、provider 的公开参数投影与配置 fingerprint;参数只经投影落盘,token/凭据永不进来。 */
28
- sandbox?: {
29
- provider: string;
30
- params?: Record<string, JsonValue>;
31
- fingerprint?: string;
32
- };
28
+ sandbox?: SandboxRunInfo;
29
+ /** spec 携带 environments 表时:声明了 environment 的选中 eval 各自解析到的产物投影,按 eval id 留审计映射;其余 eval 以 `sandbox` 为准。 */
30
+ sandboxByEval?: Record<string, SandboxRunInfo>;
31
+ }
32
+ export interface SandboxRunInfo {
33
+ provider: string;
34
+ params?: Record<string, JsonValue>;
35
+ fingerprint?: string;
33
36
  }
34
37
  /**
35
38
  * 一次 attempt 的生命周期词表——**全仓唯一一套**(见 docs/feature/results/architecture.md
@@ -309,6 +312,8 @@ export interface EvalDef {
309
312
  description?: string;
310
313
  /** 标签,供 CLI `--tag` 过滤和 view 分类;与 id 前缀过滤是两套独立的筛选维度。 */
311
314
  tags?: string[];
315
+ /** 这条 eval 需要的环境 profile id(provider-neutral,如 `"python-3.9-astropy-4.2"`);由 sandbox spec 的 `environments` 表翻译成该 provider 的预制产物。 */
316
+ environment?: string;
312
317
  /** 覆盖项目级 Config.judge,只对这一个 eval 生效(如换个更贵的评审模型)。 */
313
318
  judge?: JudgeConfig;
314
319
  /** 覆盖 / 追加项目级 Config.reporters,只对这一个 eval 生效。 */
@@ -377,7 +382,10 @@ export interface ExperimentDef {
377
382
  evals?: "*" | string[] | ((id: string) => boolean);
378
383
  /** 覆盖项目级 / CLI 的单次 attempt 超时(毫秒),只对这个实验生效。 */
379
384
  timeoutMs?: number;
380
- /** 覆盖项目级 Config.sandbox,只对这个实验生效。 */
385
+ /**
386
+ * 覆盖项目级 Config.sandbox,只对这个实验生效。固定 SandboxSpec 对全部选中 eval 复用;
387
+ * spec 可携带 `environments` 表,按 eval 的 `environment` profile 换预制产物。
388
+ */
381
389
  sandbox?: SandboxOption;
382
390
  /**
383
391
  * 本实验的花费上限(USD)。调度器按「已完成 attempt 的实测花费」累计,到顶后跳过这个实验
@@ -474,6 +482,8 @@ export interface AgentRun {
474
482
  runs: number;
475
483
  earlyExit: boolean;
476
484
  sandbox?: SandboxOption;
485
+ /** environments 查表的规划期缓存(只含声明了 environment 的 selected eval);每条只派生一次。 */
486
+ resolvedSandboxes?: Map<string, SandboxOption>;
477
487
  timeoutMs?: number;
478
488
  budget?: number;
479
489
  evalFilter: (id: string) => boolean;
@@ -534,6 +544,8 @@ export interface Attempt {
534
544
  /** agent+model+evalId,用于首过即停。 */
535
545
  key: string;
536
546
  fingerprint: string;
547
+ /** 规划期按 eval 的 environment 查表派生的具体 spec;attempt 生命周期不再重新查表。 */
548
+ sandboxSpec?: SandboxOption;
537
549
  /**
538
550
  * 构造 fresh attempt plan 时即算好的 Attempt 定位符(不是完成后写回):由 invocation 的
539
551
  * snapshotStartedAt 与 attempt 身份派生,贯穿执行、留存登记与落盘——登记项、run 收尾反馈与
@@ -109,18 +109,30 @@ export interface DockerSandboxSpec extends SandboxHooks<DockerSandboxSpec> {
109
109
  readonly provider: "docker";
110
110
  /** 覆盖默认镜像;默认按 runtime 选 `node:*-slim`。预制模板:传烘焙好 agent CLI 的镜像名。 */
111
111
  readonly image?: string;
112
+ /** 按 eval 的 `environment` profile 覆盖预制镜像:键为 profile id,值为该 profile 起步的镜像。未声明 environment 的 eval 用 `image`。 */
113
+ readonly environments?: Readonly<Record<string, {
114
+ readonly image: string;
115
+ }>>;
112
116
  readonly runtime?: SandboxRuntime;
113
117
  }
114
118
  export interface VercelSandboxSpec extends SandboxHooks<VercelSandboxSpec> {
115
119
  readonly provider: "vercel";
116
120
  /** 从已有快照起 microVM。预制模板:烘焙好 agent CLI 的 snapshotId。 */
117
121
  readonly snapshotId?: string;
122
+ /** 按 eval 的 `environment` profile 覆盖预制快照:键为 profile id,值为该 profile 起步的 snapshotId。未声明 environment 的 eval 用 `snapshotId`。 */
123
+ readonly environments?: Readonly<Record<string, {
124
+ readonly snapshotId: string;
125
+ }>>;
118
126
  readonly runtime?: SandboxRuntime;
119
127
  }
120
128
  export interface E2BSandboxSpec extends SandboxHooks<E2BSandboxSpec> {
121
129
  readonly provider: "e2b";
122
130
  /** e2b 模板名/ID。预制模板:烘焙好 agent CLI 的模板(如 `"niceeval-agents"`)。省略用 e2b 默认 `"base"`。 */
123
131
  readonly template?: string;
132
+ /** 按 eval 的 `environment` profile 覆盖预制模板:键为 profile id,值为该 profile 起步的模板。未声明 environment 的 eval 用 `template`。 */
133
+ readonly environments?: Readonly<Record<string, {
134
+ readonly template: string;
135
+ }>>;
124
136
  /** 仅作记录;e2b 的 node 版本由模板决定,不在创建时选。 */
125
137
  readonly runtime?: SandboxRuntime;
126
138
  }
@@ -18,7 +18,7 @@ export declare function experimentGroupOf(experimentId: string): string | undefi
18
18
  /**
19
19
  * eval id 前缀过滤,同 CLI 位置参数语义(docs/feature/reports/show.md「打开与收窄」):
20
20
  * eval 位置参数是收窄过滤,按**裸前缀宽松匹配**——"algebra" 命中 "algebra"、"algebra/..."
21
- * 也命中 "algebra2",多命中正是它的用途(与 `--experiment` 的按路径段匹配有意不同)。
21
+ * 也命中 "algebra2",多命中正是它的用途(与 `--exp` 的按路径段匹配有意不同)。
22
22
  */
23
23
  export declare function evalPrefixPredicate(evals?: string | string[]): (id: string) => boolean;
24
24
  /** 无 experimentId 时的兜底标签。 */
@@ -38,7 +38,7 @@ export function experimentGroupOf(experimentId) {
38
38
  /**
39
39
  * eval id 前缀过滤,同 CLI 位置参数语义(docs/feature/reports/show.md「打开与收窄」):
40
40
  * eval 位置参数是收窄过滤,按**裸前缀宽松匹配**——"algebra" 命中 "algebra"、"algebra/..."
41
- * 也命中 "algebra2",多命中正是它的用途(与 `--experiment` 的按路径段匹配有意不同)。
41
+ * 也命中 "algebra2",多命中正是它的用途(与 `--exp` 的按路径段匹配有意不同)。
42
42
  */
43
43
  export function evalPrefixPredicate(evals) {
44
44
  if (evals === undefined)
@@ -28,6 +28,7 @@ export default defineEval({
28
28
  | 字段 | 说明 |
29
29
  |---|---|
30
30
  | `description` | 给人看的描述,出现在报告里 |
31
+ | `environment` | 可选的环境需求 profile,由 sandbox spec 的 `environments` 表映射到具体预制环境 |
31
32
  | `test(t)` | 交互和断言逻辑 |
32
33
 
33
34
  eval 本身不声明用哪个 Agent——它默认保持 agent-neutral,同一个 eval 可以在不同 experiment 下跑不同 Agent。选哪个 Agent 是 experiment 的字段,不由 CLI 临时覆盖。
@@ -45,7 +46,7 @@ npx niceeval exp local weather
45
46
  npx niceeval exp local weather/brooklyn
46
47
  ```
47
48
 
48
- 这种方式让 ID 稳定、可读,并自然跟目录结构保持一致。
49
+ 这种方式让 ID 稳定、可读,并自然跟目录结构保持一致。位置参数采用裸字符串前缀:`terminal-swe-bench` 也会命中 `terminal-swe-bench-astropy-1`,不要求后一个字符必须是 `/`。
49
50
 
50
51
  ## 生命周期
51
52
 
@@ -27,6 +27,8 @@ export default defineExperiment({
27
27
 
28
28
  Experiment 是纯配置数据,没有 `setup` / `teardown` 这类生命周期字段。要按实验准备环境(装二进制、预热、跨 attempt 存取状态),挂在 `sandbox` 字段的 spec 上——`dockerSandbox()` 等工厂返回的对象可以链 `.setup()` / `.teardown()`,见 [沙箱 provider · 环境钩子](/zh/how-to/sandbox-providers#环境钩子)。
29
29
 
30
+ 同一实验里的 eval 需要不同预制环境时,eval 只声明 provider-neutral 的 `environment` profile;sandbox spec 的 `environments` 表再把 profile 映射到 Docker image、E2B template 或 Vercel sandbox snapshot。这样任务需求留在 eval,provider 产物留在 spec,一个实验仍覆盖全部 eval、对比不拆分。写法见[写实验 · 让不同 eval 使用不同预制环境](/zh/how-to/write-experiment#让不同-eval-使用不同预制环境)。
31
+
30
32
  ## 矩阵对比
31
33
 
32
34
  要比较的每个变体写一个 experiment 文件:两个模型就是两个文件,只差 `model` 一行;prompt A/B 就是只差一个参数。同一批 eval 在多个 experiment 下各跑一遍,pass rate、成本、延迟就有了可比的横截面——`niceeval view` 里叠着看。适合比什么、结果怎么读,见[实验矩阵](/zh/how-to/experiments)。
@@ -8,12 +8,28 @@ description: "一份报告就是一个报告文件:官方宿主打开结果、
8
8
 
9
9
  一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report`,终端和网页两扇门就都认它——官方的证据深链、`--results` 换根、静态导出,自定义报告全部原样享有。
10
10
 
11
- ## show / view 的默认报告也是一份报告定义
11
+ ## show / view 的默认报告也是一份报告文件
12
12
 
13
- `niceeval show` / `view` 不传 `--report` 时渲染的默认报告不是私有实现,而是 `niceeval/report` 公开导出的一个值:
13
+ `niceeval show` / `view` 不传 `--report` 时渲染的默认报告不是私有实现,而是包里自带的一个普通报告文件:三个页面(报告、Attempts、追踪),每页由 `niceeval/report` 公开导出的组件搭成——页首的标题区(`Hero`)、选择警告(`ScopeWarnings`),首页的比较组件 `ExperimentComparison`,Attempts 页的 Attempt 列表(`AttemptList`),追踪页的追踪瀑布(`TraceWaterfall`)。
14
+
15
+ 这份默认报告本身以 `standard` 为名从 `niceeval/report/built-in` 导出。只想在默认报告上加站点标题、GitHub 链接或统计脚本时,用 `extends` 在它上面叠自己的外壳,页面内容一行不用写;NiceEval 升级带来的页面改进也会自动跟过来:
16
+
17
+ ```tsx
18
+ // reports/branded.tsx —— 默认报告整站 + 自己的标题和链接
19
+ import { defineReport } from "niceeval/report";
20
+ import { standard } from "niceeval/report/built-in";
21
+
22
+ export default defineReport({
23
+ extends: standard,
24
+ title: "Memory Evals",
25
+ links: [{ label: "GitHub", href: "https://github.com/you/repo" }],
26
+ });
27
+ ```
28
+
29
+ `niceeval/report/built-in` 是内置报告的集合,每份一个名字;今天只有 `standard`,以后新增的内置报告也从这里按名字导入。想改页面内容本身,用同一批公开组件自己搭——你的报告文件能逐字写出同样的页面,也能只留自己要的部分:
14
30
 
15
31
  ```ts
16
- import { ExperimentComparison } from "niceeval/report";
32
+ import { ExperimentComparison, Hero, ScopeWarnings } from "niceeval/report";
17
33
  ```
18
34
 
19
35
  `ExperimentComparison` 先按 experiment id 的父目录切成可比组,再为每组分别计算成本 × 端到端成功率散点图和实验明细表。`compare/bub` 与 `compare/codex` 可以同图,`dev-e2b/bub` 必须在另一个组;顶层 experiment 各自成为单例组。网页持有全部组并一次聚焦一组;终端命中多组时只列索引和单组查看命令,命中单组时才展开详情。两面都不会生成跨组总榜。
@@ -63,13 +79,13 @@ niceeval view --report reports/exam.tsx # 网页:同一棵树走网页面
63
79
 
64
80
  报告文件里有两种数据形态。实体列表的 `.data(selection)` 返回普通数组:`ExperimentListItem[]`、`EvalListItem[]`、`AttemptListItem[]`。报告作者用 JavaScript `.filter()` / `.slice()` 决定展示哪些实体,再把数组作为 `items` 传给列表;组件不藏另一套过滤 DSL。指标图形和汇总组件收算好的 `data`;其中 `MetricScatter` 也提供 `selection` 简写,由宿主在渲染前计算。计算后的组件只渲染传入数据,不碰结果目录。
65
81
 
66
- 挑选提醒(覆盖不全、快照过期、有没跑完的运行)不用你在报告里专门摆一个组件来显示:不管你的报告摆了哪些组件,`niceeval show` 和 `niceeval view` 都会把 `selection.warnings` 显示在报告正文上方,不会因为你没摆某一块就静默丢掉。
82
+ 挑选提醒(覆盖不全、快照过期、有没跑完的运行、读不出来的快照)由选择警告(`ScopeWarnings`)组件显示:提醒按实验归组,组头列出实验名、问题标签和一条可复制的重跑命令,每条提醒的原文收在组内可展开的折叠块里(提醒少时默认展开),读不出来的快照单独归成一组。默认报告每一页都摆了它;自己的报告想显示提醒,就在页首摆一个 `<ScopeWarnings />`——不摆就不显示,数字是否附带完整性提醒由你对读者负责。
67
83
 
68
- 命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--results` 把结果根换成指定目录,`--experiment` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
84
+ 命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--results` 把结果根换成指定目录,`--exp` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
69
85
 
70
86
  页面里的每个组件都是**双面**的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据。实体列表按 experiment → Eval → Attempt 展示事实;指标表、矩阵、条形图、成绩单、散点图、趋势图和差异表展示聚合值。完整清单见[报告组件](/zh/reference/report-components)。网页面的实体、格子和点深链到 Attempt 详情,终端面印出对应的 `niceeval show <eval id>` 下钻命令。
71
87
 
72
- 默认报告没有特权:它就是上面导出的 `ExperimentComparison`。你的报告和它同级;需要同样的“按目录分组、组内比较”摆法就直接引用它,不需要就不摆。自己直接组合 `MetricScatter` / `ExperimentList` 时,通用组件只消费你传入的数据,不会自动分组;把跨组 Selection 传进去就表示你明确要做跨组分析。
88
+ 默认报告没有特权:它的三个页面全部由公开组件搭成,你的报告和它同级。需要同样的“按目录分组、组内比较”摆法就直接写 `<ExperimentComparison />`,需要 Attempts 页就写带过滤的 `<AttemptList />`,不需要就不摆——包括页首标题区和 `Powered by NiceEval` 品牌行:它们分别是 `Hero` 和 `PoweredBy` 组件,用了就带、不用就没有,组件本身不提供开关。自己直接组合 `MetricScatter` / `ExperimentList` 时,通用组件只消费你传入的数据,不会自动分组;把跨组数据传进去就表示你明确要做跨组分析。
73
89
 
74
90
  ## 排版:Row 与 Col 在两个面都成立
75
91
 
@@ -395,4 +411,4 @@ niceeval view --report reports/exam.tsx --out site
395
411
 
396
412
  ## 界线:内置命令不长配置
397
413
 
398
- 一次只渲染一份报告,`--report` 收显式文件路径——没有 `reports/` 目录自动发现、没有插件注册表、没有配置文件。自定义指标和自定义组件都住在你的报告文件里,随文件一起递入,宿主不为它们长任何注册面。不传 `--report` 时渲染的就是内置的 `ExperimentComparison`,你的报告和它是同级实现。
414
+ 一次只渲染一份报告,`--report` 收显式文件路径——没有 `reports/` 目录自动发现、没有插件注册表、没有配置文件。自定义指标和自定义组件都住在你的报告文件里,随文件一起递入,宿主不为它们长任何注册面。不传 `--report` 时渲染的就是默认报告,你的报告和它是同级实现。
@@ -68,11 +68,11 @@ api-validation claude-code pass@3 = 1/3 (33%) mean 41s
68
68
  除了 pass rate,还应该看平均耗时、token、成本和失败类型。
69
69
 
70
70
  ```bash
71
- npx niceeval show --experiment compare-models
71
+ npx niceeval show --exp compare-models
72
72
  npx niceeval view
73
73
  ```
74
74
 
75
- `show --experiment` 按路径段匹配 id 前缀,所以组名会选中组内所有配置,但不会误中名字只是在字符串上相似的另一个组。`view` 默认加载完整结果,直接在页面里选组;`--experiment` 对它只是可选的启动时收窄。
75
+ `show --exp` 按路径段匹配 id 前缀,所以组名会选中组内所有配置,但不会误中名字只是在字符串上相似的另一个组。`view` 默认加载完整结果,直接在页面里选组;`--exp` 对它只是可选的启动时收窄。
76
76
 
77
77
  ## 设计 experiment 的建议
78
78
 
@@ -31,11 +31,17 @@ await copySnapshots(results.latest(), output, {
31
31
  npx niceeval view --results report-data --out site
32
32
  ```
33
33
 
34
+ 只发布一部分实验时,把收窄直接交给导出命令,页面和证据都只含收窄后的范围(见[查看结果 · 导出与静态托管](/zh/how-to/viewing-results#导出与静态托管)):
35
+
36
+ ```bash
37
+ npx niceeval view --results report-data --exp compare --out site
38
+ ```
39
+
34
40
  `view` 对零可读结果直接报错、非零退出,不会导出一张空报告——`report-data/` checkout 坏掉,或所有落盘与当前 niceeval 的 schemaVersion 不兼容被整批跳过时,构建失败,Vercel / GitHub Pages 保留上一次部署。错误逐条列出被跳过的快照目录与原因,schemaVersion 场景还给出能直接查看旧落盘的 `npx niceeval@<版本> view` 命令。
35
41
 
36
42
  ## 发布自定义报告
37
43
 
38
- 不传 `--report` 时,发布出来的首页是默认报告。想让首页换成自己的报告,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 证据页(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
44
+ 不传 `--report` 时,发布出来的站点就是默认报告的三个页面(报告、Attempts、追踪)。想换成自己的页面,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 详情(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
39
45
 
40
46
  ```bash
41
47
  npx niceeval view --results report-data --report reports/exam.tsx --out site
@@ -45,12 +51,14 @@ npx niceeval view --results report-data --report reports/exam.tsx --out site
45
51
 
46
52
  ## 接站点分析与第三方脚本
47
53
 
48
- 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目:
54
+ 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目。页面内容不用重写:默认报告以 `standard` 为名从 `niceeval/report/built-in` 导出,`extends` 它就是原样的默认站点加上你的标题和脚本:
49
55
 
50
56
  ```tsx
51
- import { ExperimentComparison, defineReport } from "niceeval/report";
57
+ import { defineReport } from "niceeval/report";
58
+ import { standard } from "niceeval/report/built-in";
52
59
 
53
60
  export default defineReport({
61
+ extends: standard,
54
62
  title: "Memory Evals",
55
63
  head: [
56
64
  { tag: "script", attrs: { async: true, src: "https://www.googletagmanager.com/gtag/js?id=G-XXXX" } },
@@ -64,7 +72,6 @@ export default defineReport({
64
72
  `,
65
73
  },
66
74
  ],
67
- content: <ExperimentComparison />,
68
75
  });
69
76
  ```
70
77