niceeval 0.7.0 → 0.8.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 (34) hide show
  1. package/dist/report/index.d.ts +1 -1
  2. package/dist/report/report.d.ts +23 -1
  3. package/dist/report/report.js +82 -5
  4. package/docs-site/zh/how-to/custom-reports.mdx +3 -3
  5. package/docs-site/zh/how-to/publish-report.mdx +42 -5
  6. package/docs-site/zh/how-to/viewing-results.mdx +4 -4
  7. package/docs-site/zh/reference/cli.mdx +2 -2
  8. package/docs-site/zh/reference/results-data.mdx +1 -1
  9. package/docs-site/zh/troubleshooting/debugging.mdx +2 -2
  10. package/package.json +7 -6
  11. package/src/report/index.ts +1 -0
  12. package/src/report/report.ts +128 -6
  13. package/src/report/shell-head.test.ts +102 -0
  14. package/src/show/report-host.ts +13 -0
  15. package/src/view/app/components/CodeView.test.tsx +142 -0
  16. package/src/view/app/components/CodeView.tsx +15 -1
  17. package/src/view/app/components/Transcript.tsx +28 -1
  18. package/src/view/app/i18n.ts +6 -0
  19. package/src/view/app/lib/artifact-url.ts +14 -3
  20. package/src/view/app/lib/guards.test.ts +108 -0
  21. package/src/view/app/lib/guards.ts +13 -3
  22. package/src/view/app/lib/transcript-data.tsx +14 -0
  23. package/src/view/app/types.ts +17 -1
  24. package/src/view/artifact-serving.test.ts +21 -1
  25. package/src/view/client-dist/app.css +1 -1
  26. package/src/view/client-dist/app.js +20 -20
  27. package/src/view/data.ts +54 -3
  28. package/src/view/index.ts +18 -47
  29. package/src/view/server.ts +49 -142
  30. package/src/view/site-head.test.ts +177 -0
  31. package/src/view/site-parity.test.ts +117 -0
  32. package/src/view/site.ts +209 -0
  33. package/src/view/styles.css +10 -0
  34. package/src/view/view-report.test.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  export { assistantTurns, costUSD, defineMetric, durationMs, endToEndPassRate, examScore, executionReliability, repeatedFailedCommands, taskPassRate, tokens, } from "./metrics.ts";
2
2
  export { flag, numericFlag, numericRunConfig, runConfig } from "./flag.ts";
3
3
  export { buildReportMeta, defineReport, isReportDefinition, pickReportPage, renderReportToText, renderReportTreeToText, reportTitleText, resolveReportTitle, ReportPageNotFoundError, DEFAULT_PAGE_ID, } from "./report.ts";
4
- export type { HostCommandContext, NonEmptyArray, RenderReportTextOptions, RenderTreeTextOptions, ReportTreeHostContext, ReportAsset, ReportDef, ReportDefinition, ReportHostContext, ReportLink, ReportMeta, ReportPage, ReportShell, } from "./report.ts";
4
+ export type { HostCommandContext, NonEmptyArray, RenderReportTextOptions, RenderTreeTextOptions, ReportTreeHostContext, HeadTag, ReportAsset, ReportDef, ReportDefinition, ReportHostContext, ReportLink, ReportMeta, ReportPage, ReportShell, } from "./report.ts";
5
5
  export { defineComponent, createTextContext, renderNodeToText, resolveReportTree, validateReportTree, ResolveMemo } from "./tree.ts";
6
6
  export type { ComponentFaces, ComposeContext, ReportComponent, ReportElement, ReportNode, ResolveContext, ResolveEnv, TextContext, TextRenderOptions, WebContext, } from "./tree.ts";
7
7
  export { Col, Row, Section, Style, Tab, Table, Tabs, Text } from "./primitives.tsx";
@@ -21,6 +21,21 @@ export type ReportAsset = {
21
21
  inline: string;
22
22
  src?: never;
23
23
  };
24
+ /**
25
+ * 结构化 head 标签。tag 是白名单闭集——head 是元数据与第三方脚本的注入口,不是 HTML 后门。
26
+ * attrs 值为 true 渲染裸布尔属性(async、defer),字符串渲染 `key="value"`(值转义后落 HTML);
27
+ * 属性语义与脚本内容同一约定——作者义务,宿主不校验。
28
+ * meta / link 无子内容由类型表达;script / style 的 children 是原样文本,不转义。
29
+ */
30
+ export type HeadTag = {
31
+ tag: "meta" | "link";
32
+ attrs: Record<string, string | true>;
33
+ children?: never;
34
+ } | {
35
+ tag: "script" | "style";
36
+ attrs?: Record<string, string | true>;
37
+ children?: string;
38
+ };
24
39
  export interface ReportShell {
25
40
  /** 标题:首页 hero 与浏览器标题。页头左端是恒定的 NiceEval 品牌字标,不由 title 覆盖;回退链 def.title → 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」。 */
26
41
  title?: LocalizedText;
@@ -28,6 +43,12 @@ export interface ReportShell {
28
43
  links?: ReportLink[];
29
44
  /** 每页页脚的一段文字;省略时不渲染页脚(品牌行恒在 hero 下方,不占页脚)。 */
30
45
  footer?: LocalizedText;
46
+ /**
47
+ * 注入每页 `<head>` 的结构化标签,在官方与外壳样式之后按声明顺序渲染。
48
+ * 第三方 snippet(分析、埋点、评论)、SEO meta、favicon、字体、JSON-LD 的家:
49
+ * 声明什么标签就渲染什么标签,宿主只做结构校验,新的第三方接入不需要契约变更。
50
+ */
51
+ head?: HeadTag[];
31
52
  /** 注入每个页面的脚本,在官方增强脚本之后、按声明顺序于 </body> 前加载。 */
32
53
  scripts?: ReportAsset[];
33
54
  /** 注入每个页面的样式表,在官方样式之后按声明顺序加载。 */
@@ -53,13 +74,14 @@ export type ReportDef = ReportShell & ({
53
74
  /**
54
75
  * defineReport 的唯一产物:只作 --report 文件的默认导出,交给宿主装载。
55
76
  * 它不是 ReportNode——不能放进任何 content 或报告树,外壳因此不可嵌套。
56
- * 字段是装载规范化后的形态:pages 恒非空,links / scripts / styles 恒为数组。
77
+ * 字段是装载规范化后的形态:pages 恒非空,links / head / scripts / styles 恒为数组。
57
78
  */
58
79
  export interface ReportDefinition {
59
80
  readonly kind: "report";
60
81
  readonly title?: LocalizedText;
61
82
  readonly links: readonly ReportLink[];
62
83
  readonly footer?: LocalizedText;
84
+ readonly head: readonly HeadTag[];
63
85
  readonly scripts: readonly ReportAsset[];
64
86
  readonly styles: readonly ReportAsset[];
65
87
  readonly pages: NonEmptyArray<ReportPage>;
@@ -1,4 +1,4 @@
1
- // defineReport:唯一可被宿主装载的产物 —— 一层外壳(标题、外链、页脚、脚本、样式)加
1
+ // defineReport:唯一可被宿主装载的产物 —— 一层外壳(标题、外链、页脚、head 标签、脚本、样式)加
2
2
  // 非空页列表;单页与多页不是两种机制,页数只是列表长度(docs/feature/reports/library/shell.md)。
3
3
  // 入参有两级缩写,各有精确展开:树入参 ≡ { content: 树 } ≡ pages: [{ id: "report",
4
4
  // title: 内置页名, content: 树 }]。`content` 与 `pages` 恰好声明一个,没有隐式默认。
@@ -51,6 +51,13 @@ function assertLocalizedText(value, where) {
51
51
  throw new Error(`${where} must be a LocalizedText (a string, or a { [locale]: string } record); got ${typeof value}.`);
52
52
  }
53
53
  const PAGE_ID_PATTERN = /^[a-z0-9-]+$/;
54
+ /** 本地资产路径纪律(shell.md「行为约束」):相对报告文件的普通相对路径,拒绝 `..` 段、绝对路径与 `~`。 */
55
+ function assertLocalAssetPath(src, where) {
56
+ const segments = src.split(/[\\/]+/);
57
+ if (src.startsWith("/") || /^[A-Za-z]:/.test(src) || src.startsWith("~") || segments.includes("..")) {
58
+ throw new Error(`defineReport ${where} "${src}" is not allowed: only plain relative paths (optionally with a ./ prefix) resolve against the report file — no ".." segments, absolute paths, or "~". Move the asset next to the report file and reference it relatively.`);
59
+ }
60
+ }
54
61
  function assertAssets(assets, field) {
55
62
  if (assets === undefined)
56
63
  return [];
@@ -65,21 +72,90 @@ function assertAssets(assets, field) {
65
72
  }
66
73
  if (hasSrc) {
67
74
  const src = asset.src;
68
- const segments = src.split(/[\\/]+/);
69
- if (src.startsWith("/") || /^[A-Za-z]:/.test(src) || src.startsWith("~") || segments.includes("..")) {
70
- throw new Error(`defineReport ${field} src "${src}" is not allowed: only plain relative paths (optionally with a ./ prefix) resolve against the report file no ".." segments, absolute paths, or "~". Move the asset next to the report file and reference it relatively.`);
75
+ // 外链不属于增强层资产:第三方外链标签的家是 head 通道。
76
+ if (/^https?:\/\//i.test(src) || src.startsWith("//")) {
77
+ throw new Error(`defineReport ${field} src "${src}" is an external URL ${field} take local files and inline content (the host pipeline vendors them). Declare third-party external tags in "head" instead, e.g. head: [{ tag: "script", attrs: { async: true, src: "…" } }].`);
71
78
  }
79
+ assertLocalAssetPath(src, `${field} src`);
72
80
  }
73
81
  }
74
82
  return assets;
75
83
  }
84
+ const HEAD_TAG_NAMES = new Set(["meta", "link", "script", "style"]);
85
+ const HEAD_ATTR_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_.:-]*$/;
86
+ function assertHeadTags(tags) {
87
+ if (tags === undefined)
88
+ return [];
89
+ if (!Array.isArray(tags)) {
90
+ throw new Error('defineReport head must be an array of { tag, attrs?, children? } entries (tag: "meta" | "link" | "script" | "style").');
91
+ }
92
+ for (const entry of tags) {
93
+ const tag = entry?.tag;
94
+ // 白名单闭集:head 是元数据与第三方脚本的注入口,不是 HTML 后门;标题走 title 字段回退链。
95
+ if (typeof tag !== "string" || !HEAD_TAG_NAMES.has(tag)) {
96
+ throw new Error(`defineReport head tag ${JSON.stringify(tag)} is not allowed — head injects metadata and third-party tags, and the allowed tags are "meta", "link", "script", "style". For the document title, use the shell "title" field instead.`);
97
+ }
98
+ const attrs = entry.attrs;
99
+ if (attrs !== undefined && (typeof attrs !== "object" || attrs === null || Array.isArray(attrs))) {
100
+ throw new Error(`defineReport head <${tag}> attrs must be a { name: string | true } record (true renders a bare boolean attribute like async).`);
101
+ }
102
+ if ((tag === "meta" || tag === "link") && attrs === undefined) {
103
+ throw new Error(`defineReport head <${tag}> needs attrs — a bare <${tag}> renders nothing. Declare e.g. { tag: "${tag}", attrs: { ${tag === "meta" ? 'name: "…", content: "…"' : 'rel: "…", href: "…"'} } }.`);
104
+ }
105
+ const attrRecord = (attrs ?? {});
106
+ for (const [name, value] of Object.entries(attrRecord)) {
107
+ if (!HEAD_ATTR_NAME_PATTERN.test(name)) {
108
+ throw new Error(`defineReport head <${tag}> attribute name ${JSON.stringify(name)} is not a valid HTML attribute name. Use letters, digits, "-", "_", ":" or ".".`);
109
+ }
110
+ if (value !== true && typeof value !== "string") {
111
+ throw new Error(`defineReport head <${tag}> attribute "${name}" must be a string or true (true renders a bare boolean attribute like async); got ${typeof value}.`);
112
+ }
113
+ }
114
+ // 宿主自有的文档单例:charset / viewport 由宿主外壳拥有,声明它们装载报错。
115
+ if (tag === "meta" && attrRecord.charset !== undefined) {
116
+ throw new Error("defineReport head must not declare <meta charset> — the document charset is owned by the host shell. Remove the entry.");
117
+ }
118
+ if (tag === "meta" && typeof attrRecord.name === "string" && attrRecord.name.toLowerCase() === "viewport") {
119
+ throw new Error('defineReport head must not declare <meta name="viewport"> — the viewport is owned by the host shell. Remove the entry.');
120
+ }
121
+ const children = entry.children;
122
+ if (children !== undefined) {
123
+ if (tag === "meta" || tag === "link") {
124
+ throw new Error(`defineReport head <${tag}> does not take children — <${tag}> is a void element; put the content in attrs.`);
125
+ }
126
+ if (typeof children !== "string") {
127
+ throw new Error(`defineReport head <${tag}> children must be a string of literal ${tag === "script" ? "JavaScript" : "CSS"}; got ${typeof children}.`);
128
+ }
129
+ // children 原样落进标签,闭合序列在该上下文无法转义,会提前截断标签。
130
+ if (children.toLowerCase().includes(`</${tag}`)) {
131
+ throw new Error(`defineReport head <${tag}> children contain "</${tag}>" — that sequence cannot be escaped inside a <${tag}> and would close the tag early. Split the content into two entries or move it into a local file asset.`);
132
+ }
133
+ }
134
+ // src / href 按 scheme 分流:http(s) 外链原样透传;其余按本地路径纪律解析。
135
+ for (const name of ["src", "href"]) {
136
+ const value = attrRecord[name];
137
+ if (typeof value !== "string")
138
+ continue;
139
+ if (/^https?:\/\//i.test(value))
140
+ continue;
141
+ if (value.startsWith("//")) {
142
+ throw new Error(`defineReport head <${tag}> ${name} "${value}" is protocol-relative — declare the scheme explicitly, e.g. "https:${value}".`);
143
+ }
144
+ if (/^[a-z][a-z0-9+.-]*:/i.test(value)) {
145
+ throw new Error(`defineReport head <${tag}> ${name} "${value}" uses a scheme other than http(s) — external head assets must be http(s) URLs. Anything else, ship as a local file next to the report and reference it relatively.`);
146
+ }
147
+ assertLocalAssetPath(value, `head <${tag}> ${name}`);
148
+ }
149
+ }
150
+ return tags;
151
+ }
76
152
  export function defineReport(input) {
77
153
  assertNotDefinition(input, "defineReport(...)");
78
154
  const def = isReportNodeInput(input)
79
155
  ? { content: input }
80
156
  : input;
81
157
  if (typeof def !== "object" || def === null) {
82
- throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, scripts?, styles?, content | pages }). " +
158
+ throw new Error("defineReport expects a report tree or a config object ({ title?, links?, footer?, head?, scripts?, styles?, content | pages }). " +
83
159
  CONTENT_NEXT_STEP);
84
160
  }
85
161
  const hasContent = "content" in def && def.content !== undefined;
@@ -143,6 +219,7 @@ export function defineReport(input) {
143
219
  ...(def.title !== undefined ? { title: def.title } : {}),
144
220
  links: [...links],
145
221
  ...(def.footer !== undefined ? { footer: def.footer } : {}),
222
+ head: assertHeadTags(def.head),
146
223
  scripts: assertAssets(def.scripts, "scripts"),
147
224
  styles: assertAssets(def.styles, "styles"),
148
225
  pages: pages,
@@ -6,7 +6,7 @@ description: "一份报告就是一个报告文件:官方宿主打开结果、
6
6
 
7
7
  [查看结果](/zh/how-to/viewing-results)讲「用」:官方两扇门 `niceeval show`(终端)和 `niceeval view`(网页)怎么看。本页讲「写」:官方摆法不够时,怎么写一份自己的报告——考试成绩单、代码行数榜、质量 × 成本 frontier。
8
8
 
9
- 一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report`,终端和网页两扇门就都认它——官方的证据室深链、`--run` 历史切换、静态导出,自定义报告全部原样享有。
9
+ 一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report`,终端和网页两扇门就都认它——官方的证据深链、`--results` 换根、静态导出,自定义报告全部原样享有。
10
10
 
11
11
  ## show / view 的默认报告也是一份报告定义
12
12
 
@@ -33,7 +33,7 @@ import { ExperimentComparison } from "niceeval/report";
33
33
 
34
34
  先交代唯一的前置:报告文件是 `.tsx`,写它的项目要装 `react`(写自定义组件的 web 面还要 `@types/react`),tsconfig 里 `compilerOptions.jsx` 设为 `"react-jsx"`。裸跑 `niceeval show` / `niceeval view` 不需要这些——只有自己写报告文件才需要。
35
35
 
36
- 报告基座是 `defineReport`:宿主打开结果目录(含 `--run` 指定历史 run)、按官方口径挑好结果快照,注入给你的函数;你只负责折数据和摆积木:
36
+ 报告基座是 `defineReport`:宿主打开结果目录(含 `--results` 指定的结果根)、按官方口径挑好结果快照,注入给你的函数;你只负责折数据和摆积木:
37
37
 
38
38
  ```tsx
39
39
  // reports/exam.tsx —— 一份定义,两扇门共用
@@ -65,7 +65,7 @@ niceeval view --report reports/exam.tsx # 网页:同一棵树走网页面
65
65
 
66
66
  挑选提醒(覆盖不全、快照过期、有没跑完的运行)不用你在报告里专门摆一个组件来显示:不管你的报告摆了哪些组件,`niceeval show` 和 `niceeval view` 都会把 `selection.warnings` 显示在报告正文上方,不会因为你没摆某一块就静默丢掉。
67
67
 
68
- 命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--run` 把结果根换成指定目录,`--experiment` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
68
+ 命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--results` 把结果根换成指定目录,`--experiment` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
69
69
 
70
70
  页面里的每个组件都是**双面**的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据。实体列表按 experiment → Eval → Attempt 展示事实;指标表、矩阵、条形图、成绩单、散点图、趋势图和差异表展示聚合值。完整清单见[报告组件](/zh/reference/report-components)。网页面的实体、格子和点深链到 Attempt 详情,终端面印出对应的 `niceeval show <eval id>` 下钻命令。
71
71
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "通过 CI 发布报告"
3
3
  sidebarTitle: "CI 发布报告"
4
- description: "把经过 copySnapshots 大小预检的结果目录提交进仓库,CI 用一行 view --run 导出报告站;超大文件在 commit 前就会得到可执行错误。"
4
+ description: "把经过 copySnapshots 大小预检的结果目录提交进仓库,CI 用一行 view --results 导出报告站;超大文件在 commit 前就会得到可执行错误。"
5
5
  ---
6
6
 
7
7
  `niceeval view --out <目录>` 把查看器导出成一个纯静态目录:报告、结果快照列表、transcript、trace 瀑布,和本地 `niceeval view` 看到的完全一样(导出行为见[查看结果 · 导出与静态托管](/zh/how-to/viewing-results#导出与静态托管))。CI 发布只需要让结果数据到构建机手里,但不要直接提交本地事实根 `.niceeval/`:逐字符串截断能防一条失控输出膨胀,不能保证整个文件小于 Git host 的限制。先用 `copySnapshots` 生成经过 50 MiB 单文件预检的发布结果根,再提交这个目录。
@@ -31,7 +31,7 @@ await copySnapshots(results.latest(), output, {
31
31
  ## 构建命令就是导出命令
32
32
 
33
33
  ```bash
34
- npx niceeval view --run report-data --out site
34
+ npx niceeval view --results report-data --out site
35
35
  ```
36
36
 
37
37
  `view` 对零可读结果直接报错、非零退出,不会导出一张空报告——`report-data/` checkout 坏掉,或所有落盘与当前 niceeval 的 schemaVersion 不兼容被整批跳过时,构建失败,Vercel / GitHub Pages 保留上一次部署。错误逐条列出被跳过的快照目录与原因,schemaVersion 场景还给出能直接查看旧落盘的 `npx niceeval@<版本> view` 命令。
@@ -41,11 +41,48 @@ npx niceeval view --run report-data --out site
41
41
  不传 `--report` 时,发布出来的首页是默认报告。想让首页换成自己的报告,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 证据页(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
42
42
 
43
43
  ```bash
44
- npx niceeval view --run report-data --report reports/exam.tsx --out site
44
+ npx niceeval view --results report-data --report reports/exam.tsx --out site
45
45
  ```
46
46
 
47
47
  报告文件和 `report-data/` 一样提交在仓库里,改完版面 push,线上就跟着更新。用下面的 `vercel.json` / workflow 时,把构建命令换成这一行即可,其余配置不用动。
48
48
 
49
+ ## 接站点分析与第三方脚本
50
+
51
+ 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目:
52
+
53
+ ```tsx
54
+ import { ExperimentComparison, defineReport } from "niceeval/report";
55
+
56
+ export default defineReport({
57
+ title: "Memory Evals",
58
+ head: [
59
+ { tag: "script", attrs: { async: true, src: "https://www.googletagmanager.com/gtag/js?id=G-XXXX" } },
60
+ {
61
+ tag: "script",
62
+ children: `
63
+ window.dataLayer = window.dataLayer || [];
64
+ function gtag(){dataLayer.push(arguments);}
65
+ gtag('js', new Date());
66
+ gtag('config', 'G-XXXX');
67
+ `,
68
+ },
69
+ ],
70
+ content: <ExperimentComparison />,
71
+ });
72
+ ```
73
+
74
+ 规则只有几条:
75
+
76
+ - `tag` 支持 `meta`、`link`、`script`、`style` 四种,按声明顺序渲染进每一页的 `<head>`。SEO meta、favicon、字体、JSON-LD 都走这里。
77
+ - `attrs` 的值写 `true` 输出裸属性(`async`、`defer`),写字符串输出 `key="value"`。靠 `data-*` 属性配置的第三方脚本(评论、埋点)直接把属性抄进来。
78
+ - `src` / `href` 写 `https://` 外链时原样保留;写 `./favicon.svg` 这类相对路径时,文件会自动复制进导出站的 `assets/` 并改好引用。
79
+ - 脚本会原样发布并在读者浏览器里执行,别在里面嵌密钥。
80
+
81
+ ```tsx
82
+ // 靠 data-* 配置的埋点脚本:属性照抄
83
+ { tag: "script", attrs: { async: true, src: "https://tracker.example/t.js", "data-project": "memory-evals" } }
84
+ ```
85
+
49
86
  ## 接托管平台
50
87
 
51
88
  **Vercel**:仓库根放一个 `vercel.json`,导入项目后 push 即部署。
@@ -53,7 +90,7 @@ npx niceeval view --run report-data --report reports/exam.tsx --out site
53
90
  ```json
54
91
  {
55
92
  "installCommand": "pnpm install --frozen-lockfile",
56
- "buildCommand": "npx niceeval view --run report-data --out site",
93
+ "buildCommand": "npx niceeval view --results report-data --out site",
57
94
  "outputDirectory": "site"
58
95
  }
59
96
  ```
@@ -83,7 +120,7 @@ jobs:
83
120
  node-version: 22
84
121
  cache: pnpm
85
122
  - run: pnpm install --frozen-lockfile
86
- - run: npx niceeval view --run report-data --out site
123
+ - run: npx niceeval view --results report-data --out site
87
124
  - uses: actions/upload-pages-artifact@v3
88
125
  with:
89
126
  path: site
@@ -66,7 +66,7 @@ Sandbox 创建、setup 或 teardown 错误不依赖 trace。`result.json` 保存
66
66
 
67
67
  同一个实验多次跑会留下多份结果,不带 `@<locator>` 的默认视图只回答一个问题——「现在整体怎样」:每个 experiment × eval 取时间上最新的那份判定,同一个 experiment 跨多次运行拼出来。按前缀只重跑了部分 eval 时,其余 eval 的判定从更早的运行补齐,报告永远是全局最新,不会因为一次局部重跑变残缺。合成是有标注的:每份判定都带上它产生的时间,能看出报告是从哪几次运行拼出来的。合成结果可能混着不同版本的被测代码——所以收工判定以 `--force` 全量重跑为准,迭代途中的 `show` 负责快、收尾的全量跑负责真。
68
68
 
69
- 单个 eval 视图里,多 experiment、多 Attempt 时的断言明细块默认挑最新一次失败的 Attempt 展开;没有失败就挑最新一次。这只是一个默认展开的启发式,不是精确选择——需要精确看某一次 Attempt,复制那一行的 `@<locator>` 直接 `show` 它即可。`--experiment compare` 按路径段前缀把 Selection 收窄到整个 `compare` 组,`--experiment compare/bub` 只留一个 experiment;`--run <目录>` 钉死看某一次历史 run,`--history` 看跨 run 趋势。`--report <文件>` 同时替换 `show` / `view` 的默认报告。位置前缀、`--run`、`--experiment` 对自定义报告同样生效;`--history` 与 `--report` 互斥。
69
+ 单个 eval 视图里,多 experiment、多 Attempt 时的断言明细块默认挑最新一次失败的 Attempt 展开;没有失败就挑最新一次。这只是一个默认展开的启发式,不是精确选择——需要精确看某一次 Attempt,复制那一行的 `@<locator>` 直接 `show` 它即可。`--experiment compare` 按路径段前缀把 Selection 收窄到整个 `compare` 组,`--experiment compare/bub` 只留一个 experiment;`--results <目录>` 换结果根,`--snapshot <snapshot.json>` 只看某一次落盘的快照,`--history` 看跨 run 趋势。`--report <文件>` 同时替换 `show` / `view` 的默认报告。位置前缀、`--results`、`--experiment` 对自定义报告同样生效;`--history` 与 `--report` 互斥。
70
70
 
71
71
  `niceeval view` 的每个视图都有 CLI 对应物:
72
72
 
@@ -79,7 +79,7 @@ Sandbox 创建、setup 或 teardown 错误不依赖 trace。`result.json` 保存
79
79
  | AI 对话、thinking、Skill 加载与工具调用(有 OTel 时补时间) | `niceeval show @<locator> --execution` |
80
80
  | 单次 Attempt 的阶段耗时分解 | `niceeval show @<locator> --timing`;逐节点审计用 `--timing=full` |
81
81
  | 文件改动 | `niceeval show @<locator> --diff` |
82
- | 历史 run 列表 | `niceeval show <eval id> --history`;钉死某一次用 `--run <目录>` |
82
+ | 历史 run 列表 | `niceeval show <eval id> --history`;钉死某一次用 `view --snapshot <snapshot.json>` 或复制该行 `@<locator>` 直接 `show` |
83
83
 
84
84
  ### 默认分组比较报告
85
85
 
@@ -319,7 +319,7 @@ compare/codex-gpt-5.4 · 5 runs · passed 2/5
319
319
  2026-07-07T16-40 ✗ failed 3 attempts $0.05 gate calledTool("get_weather")
320
320
  ```
321
321
 
322
- ✓✗ 交替说明这个 eval 在抖,该修的是稳定性(被测程序或断言),反复重跑碰运气只会烧钱;连续绿转红的拐点就是回归引入的位置,用 `--run <目录>` 钉住拐点前后两次细看。时间轴只列真实执行——缓存携带的旧结果是判定的复印件,不占行,否则趋势会被复印件灌满假数据。不带 eval id 的 `niceeval show --history` 给每个 experiment 的 per-run 通过率序列,同一份趋势的榜单视角。
322
+ ✓✗ 交替说明这个 eval 在抖,该修的是稳定性(被测程序或断言),反复重跑碰运气只会烧钱;连续绿转红的拐点就是回归引入的位置,用 `view --snapshot <snapshot.json>` 钉住拐点前后两次细看。时间轴只列真实执行——缓存携带的旧结果是判定的复印件,不占行,否则趋势会被复印件灌满假数据。不带 eval id 的 `niceeval show --history` 给每个 experiment 的 per-run 通过率序列,同一份趋势的榜单视角。
323
323
 
324
324
  两次 run 的精确对比(这次修复具体翻转了哪些 eval)不做成 flag:用 `DeltaTable` 积木写一份报告递给 `--report`,几行就是一份自定义对比报告,终端和网页两扇门都认——内置命令只管固定摆法,自定义口径见[自定义报告](/zh/how-to/custom-reports)。
325
325
 
@@ -360,7 +360,7 @@ NiceEval 写入 `<dir>/index.html`,并把查看器要读取的 artifact(`sou
360
360
  - `diff.json` 和 `o11y.json` 不会被复制。查看器不读取它们,且 diff 可能达到上百 MB。
361
361
  - 用 `file://` 直接打开 `index.html` 时浏览器不允许 fetch artifact,代码视图会提示源码不可用。本地预览用 http 服务打开。
362
362
 
363
- 最简单的流程是在跑过 eval 的机器上导出,把产物目录直接部署,不需要额外步骤。要让站点随 push 自动更新,先用 `copySnapshots` 生成经过单文件预算检查的发布结果根,把它提交进仓库,再让 CI 对这个目录运行 `view --run <目录> --out`——workflow 与平台接线见[通过 CI 发布报告](/zh/how-to/publish-report)。
363
+ 最简单的流程是在跑过 eval 的机器上导出,把产物目录直接部署,不需要额外步骤。要让站点随 push 自动更新,先用 `copySnapshots` 生成经过单文件预算检查的发布结果根,把它提交进仓库,再让 CI 对这个目录运行 `view --results <目录> --out`——workflow 与平台接线见[通过 CI 发布报告](/zh/how-to/publish-report)。
364
364
 
365
365
  ## Artifact 说明
366
366
 
@@ -152,7 +152,7 @@ npx niceeval exp compare --output ci --strict --json .niceeval/ci-summary.json -
152
152
  npx niceeval view
153
153
  ```
154
154
 
155
- 打开本地结果查看器。它和 `show` 共用同一份默认报告和同一套默认选择——对每个 experiment、每个 eval,取历次运行里最新的那份判定;只补跑部分 eval 时,其余 eval 从更早的运行补齐。默认报告按 experiment id 的父目录分组,只在同组内比较。`show` 输出终端文本,`view` 输出网页并提供可交互的证据浏览;eval ID 前缀、`--experiment`、`--run` 对两者的收窄一致。完整说明见[查看结果](/zh/how-to/viewing-results)。
155
+ 打开本地结果查看器。它和 `show` 共用同一份默认报告和同一套默认选择——对每个 experiment、每个 eval,取历次运行里最新的那份判定;只补跑部分 eval 时,其余 eval 从更早的运行补齐。默认报告按 experiment id 的父目录分组,只在同组内比较。`show` 输出终端文本,`view` 输出网页并提供可交互的证据浏览;eval ID 前缀、`--experiment`、`--results` 对两者的收窄一致。完整说明见[查看结果](/zh/how-to/viewing-results)。
156
156
 
157
157
  ## `show [id-prefix...]`
158
158
 
@@ -168,7 +168,7 @@ npx niceeval show weather/brooklyn --history
168
168
 
169
169
  `show` 是终端结果入口,适合人直接阅读,也适合 coding agent 在上下文窗口里逐级下钻。位置参数选「看哪些 eval」(ID 前缀)或直接用 `@<locator>` 精确选一个 attempt;不带位置参数时显示按实验组分区的默认比较报告,指定 eval ID 前缀只收窄报告覆盖的 Eval,不改变组边界。
170
170
 
171
- `@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动,因此必须搭配 `@<locator>` 精确指名一个 attempt。`--run` 钉住某次结果目录,`--history` 查看跨 run 趋势。完整的阅读顺序、输出示例和 artifact 说明见[查看结果](/zh/how-to/viewing-results)。
171
+ `@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动,因此必须搭配 `@<locator>` 精确指名一个 attempt。`--results <目录>` 指定结果根,`--history` 查看跨 run 趋势。完整的阅读顺序、输出示例和 artifact 说明见[查看结果](/zh/how-to/viewing-results)。
172
172
 
173
173
  ## `--early-exit` 与 `--strict`
174
174
 
@@ -246,7 +246,7 @@ await copySnapshots(results.latest(), "site-data/run", {
246
246
 
247
247
  复制开始前,NiceEval 会规划全部目标文件并检查序列化后的大小。任一文件超过固定的 50 MiB,整次复制在创建目标目录前失败,错误会列出路径、实际大小和处理建议。你可以从 `artifacts` 排除那类证据;如果是旧版本留下的超大 events / trace,用当前版本重跑后再发布。这个检查既覆盖没有逐值截断的源码 / diff,也覆盖单值都正常但累计过大的 JSON,避免直到 `git push` 才撞上 Git host 的单文件限制。
248
248
 
249
- 大小预检只决定整次复制成功或失败,不会从一个超大文件中间删内容。消毒不是可选项——`copySnapshots` 要求显式传 `redact`:给一个函数就改写复制出来的所有文件里的自由文本(events、trace、源码、diff、运行摘要都在内;id、事件类型这类标识字段不动),确定这批数据可以原文公开就传 `redact: false`,两个都不传会直接报错。注意报告积木 `AttemptList.data` 的 `redact` 只影响页面上显示的数据,管不到发布目录里的 artifact 文件;发布场景一律在 `copySnapshots` 这一步消毒。唯一随行补记的是挑选时的**覆盖事实**:`partial-coverage` 警告的分母是实验的历史并集,而发布目录没有历史——所以每个复制出的快照带上 `knownEvalIds`(复制时刻该实验已知的 eval 并集),reader 端把它并进 `exp.evalIds` 的计算(取本地历史与快照携带值的并集)。发布目录上重新 `openResults().latest()`,残缺警告被同一套机制重新算出来,不靠发布者转述。复制出的目录就是标准结果目录,`niceeval view --run <目录>` 直接能看;要让报告站随 push 自动更新,workflow 见[通过 CI 发布报告](/zh/how-to/publish-report)。
249
+ 大小预检只决定整次复制成功或失败,不会从一个超大文件中间删内容。消毒不是可选项——`copySnapshots` 要求显式传 `redact`:给一个函数就改写复制出来的所有文件里的自由文本(events、trace、源码、diff、运行摘要都在内;id、事件类型这类标识字段不动),确定这批数据可以原文公开就传 `redact: false`,两个都不传会直接报错。注意报告积木 `AttemptList.data` 的 `redact` 只影响页面上显示的数据,管不到发布目录里的 artifact 文件;发布场景一律在 `copySnapshots` 这一步消毒。唯一随行补记的是挑选时的**覆盖事实**:`partial-coverage` 警告的分母是实验的历史并集,而发布目录没有历史——所以每个复制出的快照带上 `knownEvalIds`(复制时刻该实验已知的 eval 并集),reader 端把它并进 `exp.evalIds` 的计算(取本地历史与快照携带值的并集)。发布目录上重新 `openResults().latest()`,残缺警告被同一套机制重新算出来,不靠发布者转述。复制出的目录就是标准结果目录,`niceeval view --results <目录>` 直接能看;要让报告站随 push 自动更新,workflow 见[通过 CI 发布报告](/zh/how-to/publish-report)。
250
250
 
251
251
  ## 分层速览
252
252
 
@@ -190,8 +190,8 @@ niceeval view
190
190
  **打开归档或别人发来的结果。** 结果目录是自包含的——从 CI 下载的、同事拷给你的、发布到静态站前生成的目录,都能直接指过去:
191
191
 
192
192
  ```bash
193
- niceeval show --run tmp/ci-artifacts/results
194
- niceeval view --run site-data/run
193
+ niceeval show --results tmp/ci-artifacts/results
194
+ niceeval view --results site-data/run
195
195
  ```
196
196
 
197
197
  一个注意点:如果本地清理过旧快照目录,之后的运行里「沿用上次结果」的条目会找不到原始证据(显示为缺失)。要长期归档某次运行,先用 [`copySnapshots`](/zh/reference/results-data) 复制出一份再删。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niceeval",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -83,10 +83,6 @@
83
83
  },
84
84
  "devDependencies": {
85
85
  "@ai-sdk/otel": "^1.0.9",
86
- "@vercel/sandbox": "^2.2.1",
87
- "braintrust": "^3.20.0",
88
- "dockerode": "^4.0.2",
89
- "e2b": "^2.31.0",
90
86
  "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
91
87
  "@opentelemetry/sdk-trace-node": "^2.8.0",
92
88
  "@radix-ui/react-collapsible": "^1.1.14",
@@ -98,9 +94,15 @@
98
94
  "@types/react": "^19.2.17",
99
95
  "@types/react-dom": "^19.2.3",
100
96
  "@types/tar-stream": "^3.1.3",
97
+ "@typescript/native": "npm:typescript@^7.0.2",
98
+ "@vercel/sandbox": "^2.2.1",
101
99
  "@vitejs/plugin-react": "^6.0.3",
100
+ "braintrust": "^3.20.0",
102
101
  "class-variance-authority": "^0.7.1",
103
102
  "clsx": "^2.1.1",
103
+ "dockerode": "^4.0.2",
104
+ "e2b": "^2.31.0",
105
+ "jsdom": "^29.1.1",
104
106
  "lucide-react": "^1.21.0",
105
107
  "mixpanel-browser": "^2.80.0",
106
108
  "next": "16.2.10",
@@ -112,7 +114,6 @@
112
114
  "tailwind-merge": "^3.6.0",
113
115
  "tailwindcss": "^4.3.1",
114
116
  "typescript": "npm:@typescript/typescript6@^6.0.2",
115
- "@typescript/native": "npm:typescript@^7.0.2",
116
117
  "vite": "^8.1.0",
117
118
  "vitest": "^4.1.9"
118
119
  },
@@ -40,6 +40,7 @@ export type {
40
40
  RenderReportTextOptions,
41
41
  RenderTreeTextOptions,
42
42
  ReportTreeHostContext,
43
+ HeadTag,
43
44
  ReportAsset,
44
45
  ReportDef,
45
46
  ReportDefinition,