niceeval 0.8.1 → 0.9.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 (132) 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 +2 -0
  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 +66 -53
  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.js +6 -1
  38. package/dist/report/types.d.ts +45 -0
  39. package/dist/report/web.d.ts +5 -4
  40. package/dist/report/web.js +7 -22
  41. package/dist/results/select.d.ts +15 -2
  42. package/dist/results/select.js +75 -10
  43. package/dist/results/types.d.ts +26 -0
  44. package/dist/runner/fingerprint.d.ts +3 -3
  45. package/dist/runner/sandbox-selection.d.ts +12 -0
  46. package/dist/runner/types.d.ts +18 -6
  47. package/dist/sandbox/types.d.ts +12 -0
  48. package/docs-site/zh/explanation/evals.mdx +2 -1
  49. package/docs-site/zh/explanation/experiment.mdx +2 -0
  50. package/docs-site/zh/how-to/custom-reports.mdx +22 -6
  51. package/docs-site/zh/how-to/publish-report.mdx +5 -4
  52. package/docs-site/zh/how-to/viewing-results.mdx +3 -3
  53. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  54. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  55. package/docs-site/zh/reference/cli.mdx +1 -1
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/package.json +2 -1
  59. package/src/agents/bub.ts +13 -1
  60. package/src/agents/claude-code.test.ts +43 -1
  61. package/src/agents/claude-code.ts +32 -14
  62. package/src/agents/codex.test.ts +168 -1
  63. package/src/agents/codex.ts +51 -15
  64. package/src/agents/mcp.ts +31 -0
  65. package/src/agents/post-setup.ts +33 -0
  66. package/src/agents/types.ts +28 -7
  67. package/src/cli.ts +8 -6
  68. package/src/define.ts +3 -0
  69. package/src/i18n/en.ts +5 -2
  70. package/src/i18n/zh-CN.ts +5 -1
  71. package/src/index.ts +1 -0
  72. package/src/report/built-in/index.tsx +8 -7
  73. package/src/report/built-in/standard.tsx +59 -0
  74. package/src/report/components.tsx +218 -2
  75. package/src/report/compute.ts +138 -1
  76. package/src/report/dual-render.test.tsx +139 -12
  77. package/src/report/index.ts +20 -0
  78. package/src/report/locale.ts +83 -1
  79. package/src/report/react/AttemptList.tsx +13 -1
  80. package/src/report/react/CopyFixPrompt.tsx +37 -0
  81. package/src/report/react/HeroCard.tsx +59 -0
  82. package/src/report/react/PoweredBy.tsx +20 -0
  83. package/src/report/react/ScopeWarnings.tsx +74 -0
  84. package/src/report/react/TraceWaterfall.tsx +78 -0
  85. package/src/report/react/enhance.js +14 -0
  86. package/src/report/react/index.tsx +11 -0
  87. package/src/report/react/styles.css +187 -7
  88. package/src/report/report.ts +97 -62
  89. package/src/report/scope-warnings.ts +155 -0
  90. package/src/report/site-components.test.tsx +526 -0
  91. package/src/report/text/faces.ts +66 -0
  92. package/src/report/tree.ts +8 -1
  93. package/src/report/types.ts +51 -0
  94. package/src/report/web.ts +7 -40
  95. package/src/results/host-equivalence.test.ts +5 -1
  96. package/src/results/open.ts +5 -4
  97. package/src/results/results.test.ts +78 -1
  98. package/src/results/select.ts +78 -10
  99. package/src/results/types.ts +27 -0
  100. package/src/runner/attempt.ts +10 -9
  101. package/src/runner/discover.test.ts +9 -1
  102. package/src/runner/discover.ts +3 -3
  103. package/src/runner/fingerprint.ts +9 -4
  104. package/src/runner/ledger.test.ts +30 -1
  105. package/src/runner/ledger.ts +26 -4
  106. package/src/runner/run.ts +5 -1
  107. package/src/runner/sandbox-selection.test.ts +131 -0
  108. package/src/runner/sandbox-selection.ts +110 -0
  109. package/src/runner/types.ts +19 -2
  110. package/src/sandbox/types.ts +6 -0
  111. package/src/show/index.ts +17 -10
  112. package/src/show/render.ts +11 -11
  113. package/src/show/report-host.test.ts +32 -15
  114. package/src/show/report-host.ts +5 -4
  115. package/src/show/show.test.ts +140 -3
  116. package/src/view/app/App.test.tsx +78 -17
  117. package/src/view/app/App.tsx +17 -78
  118. package/src/view/app/components/CopyControls.tsx +4 -42
  119. package/src/view/app/i18n.ts +5 -227
  120. package/src/view/app/lib/rows.ts +3 -21
  121. package/src/view/app/shared.ts +1 -3
  122. package/src/view/app/types.ts +2 -2
  123. package/src/view/client-dist/app.css +1 -1
  124. package/src/view/client-dist/app.js +20 -20
  125. package/src/view/data.ts +2 -2
  126. package/src/view/shared/types.ts +11 -6
  127. package/src/view/styles.css +6 -266
  128. package/src/view/view-report.test.ts +59 -22
  129. package/src/view/app/components/LazyArtifact.tsx +0 -51
  130. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  131. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  132. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -8,16 +8,32 @@ import {
8
8
  BUILT_IN_PAGE_TITLE,
9
9
  BUILT_IN_REPORT_TITLE,
10
10
  HostReportError,
11
+ loadHostReport,
11
12
  localizeText,
12
13
  localizedTextEquals,
13
14
  normalizeHostReport,
14
15
  resolveReportTitle,
15
16
  showCommand,
16
17
  } from "./report-host.ts";
17
- import { pageIndexText } from "./render.ts";
18
+ import { otherPagesText } from "./render.ts";
19
+ // dist-sourced:裸宿主装载的就是这份预编译产物的默认导出(show 与 view 同一条路),
20
+ // raw-src import 会是另一份模块实例,引用等同断言必须对着 dist。
21
+ import distBuiltInReport from "../../dist/report/built-in/index.js";
18
22
 
19
23
  const tree = { kind: "node" }; // 页 content 对宿主是不透明值,规范化不解析树
20
24
 
25
+ describe("裸宿主装载内建报告", () => {
26
+ it("缺省(无 --report)装载 niceeval/report/built-in 的默认导出:三页与其 content 同引用", async () => {
27
+ const host = await loadHostReport(process.cwd(), undefined);
28
+ const builtIn = distBuiltInReport as { pages: readonly { id: string; content: unknown }[] };
29
+ expect(host.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
30
+ expect(builtIn.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
31
+ for (let i = 0; i < host.pages.length; i++) {
32
+ expect(host.pages[i]!.content).toBe(builtIn.pages[i]!.content); // 同一份默认导出,不是复制品
33
+ }
34
+ });
35
+ });
36
+
21
37
  describe("装载规范化:外壳 + 非空页列表", () => {
22
38
  it("content 缩写展开为唯一页 id `report`,页名是内置页名「报告 / Report」", () => {
23
39
  const report = normalizeHostReport({ kind: "report", content: tree }, "reports/frontier.tsx");
@@ -45,13 +61,13 @@ describe("装载规范化:外壳 + 非空页列表", () => {
45
61
  expect(report.footer).toBe("Published nightly.");
46
62
  });
47
63
 
48
- it("content 与 pages 恰好声明一个:同给 / 同缺都报错,文案给出 <ExperimentComparison /> 下一步", () => {
64
+ it("content 与 pages 恰好声明一个:同给 / 同缺都报错,文案给出 extends: standard 下一步", () => {
49
65
  for (const bad of [
50
66
  { kind: "report", content: tree, pages: [{ id: "a", title: "A", content: tree }] },
51
67
  { kind: "report", title: "T" },
52
68
  ]) {
53
69
  expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(HostReportError);
54
- expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(/<ExperimentComparison \/>/);
70
+ expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(/niceeval\/report\/built-in/);
55
71
  }
56
72
  });
57
73
 
@@ -141,7 +157,7 @@ describe("LocalizedText 回退:locale → en → 键字典序第一个非空值"
141
157
  });
142
158
  });
143
159
 
144
- describe("页索引与索引命令上下文", () => {
160
+ describe("其余页索引与索引命令上下文", () => {
145
161
  const report = normalizeHostReport(
146
162
  {
147
163
  kind: "report",
@@ -155,27 +171,28 @@ describe("页索引与索引命令上下文", () => {
155
171
  "reports/site.tsx",
156
172
  );
157
173
 
158
- it("索引命令保留当前 --results / --report 与位置参数,复制即可复现下一层视图", () => {
159
- const text = pageIndexText({
160
- report,
161
- title: "记忆能力评测",
174
+ it("只列未渲染的页,索引命令保留当前 --results / --report 与位置参数,复制即可复现下一层视图", () => {
175
+ // 渲染的是 overview,其余页索引只含 exam 一行——与「渲染初始页 + 尾部附其余页索引」
176
+ // 的新行为一致(docs/feature/reports/show/reports.md Case 2)。
177
+ const text = otherPagesText({
178
+ otherPages: report.pages.filter((p) => p.id !== "overview").map((p) => ({ id: p.id, title: p.title })),
162
179
  command: { patterns: [], results: "tmp/published-results", report: "reports/site.tsx" },
163
180
  locale: "zh-CN",
164
181
  });
165
- expect(text).toContain("记忆能力评测 · 2 页");
166
- expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page overview");
182
+ expect(text).toContain("其余页:");
167
183
  expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page exam");
168
- expect(text).toContain("总览");
169
184
  expect(text).toContain("成绩单");
185
+ expect(text).not.toContain("总览");
186
+ expect(text).not.toContain("--page overview");
170
187
  });
171
188
 
172
- it("show 不消费 links:页索引不含 icon svg 与 href(icon 是 web 面属性)", () => {
173
- const text = pageIndexText({
174
- report,
175
- title: "Memory Evals",
189
+ it("show 不消费 links:其余页索引不含 icon svg 与 href(icon 是 web 面属性)", () => {
190
+ const text = otherPagesText({
191
+ otherPages: report.pages.filter((p) => p.id !== "overview").map((p) => ({ id: p.id, title: p.title })),
176
192
  command: { patterns: [] },
177
193
  locale: "en",
178
194
  });
195
+ expect(text).toContain("Other pages:");
179
196
  expect(text).not.toContain("<svg");
180
197
  expect(text).not.toContain("https://example.com");
181
198
  });
@@ -7,8 +7,8 @@
7
7
  // - 装载规范化唯一产物是「外壳 + 非空页列表」:`defineReport(树)` ≡ `{ content: 树 }` ≡
8
8
  // `pages: [{ id: "report", title: 内置页名, content: 树 }]`。
9
9
  // - 标题回退单点:def.title → Scope 中唯一且相同(LocalizedText 深相等)的非空快照 name →
10
- // 内置文案「Eval 运行结果 / Eval Results」。落点是首页 hero、浏览器标题与 show 页索引标题行;
11
- // 页头品牌位恒为 NiceEval 字标,不归 title
10
+ // 内置文案「Eval 运行结果 / Eval Results」。宿主落点是浏览器标题与 show 页索引标题行;
11
+ // 页内 hero 标题由 Hero 组件消费同一取值链(ctx.report.title),宿主页头无品牌位。
12
12
  // - LocalizedText 回退:当前 locale → en → 按 locale 键字典序的第一个非空值。
13
13
  //
14
14
  // ⚠ 集成状态:src/report/** 正被并行重写(plan/reports-redesign-implementation.md)。这里把宿主
@@ -212,11 +212,12 @@ export function normalizeHostReport(definition: unknown, sourceLabel: string): H
212
212
  const hasContent = def.content !== undefined;
213
213
  const hasPages = def.pages !== undefined;
214
214
  if (hasContent === hasPages) {
215
- // content / pages 同缺或同给:装载期完整用户反馈,下一步是 content: <ExperimentComparison />。
215
+ // content / pages 同缺或同给:装载期完整用户反馈(defineReport 产物恒已折叠,这里只拦
216
+ // 手搓 kind:"report" 的无类型输入),下一步指向内建视图的 extends 复用。
216
217
  throw new HostReportError(
217
218
  `${sourceLabel}: a report declares exactly one of "content" or "pages" — ` +
218
219
  (hasContent ? "it declares both. " : "it declares neither. ") +
219
- `To render the built-in report content, write: content: <ExperimentComparison />.`,
220
+ `To render the built-in report, write extends: standard (import { standard } from "niceeval/report/built-in").`,
220
221
  );
221
222
  }
222
223
  const pages: HostReportPage[] = hasPages
@@ -163,6 +163,57 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
163
163
  expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
164
164
  });
165
165
 
166
+ it("裸 show 是内建报告首页:页首 Hero 标题行 + 最后运行 meta,尾部附 attempts / traces 页索引(命令带 --results 上下文)", async () => {
167
+ // docs/feature/reports/show/default-report.md:Hero 两行在页首,「其余页」两行在尾部。
168
+ const root = await seedComposedRoot();
169
+ const { out, code } = await show(root, []);
170
+ expect(code).toBe(0);
171
+ const lines = out.split("\n");
172
+ expect(lines[0]).toBe("Eval Results"); // 标题回退链终点(快照无 name、无 --report title)
173
+ expect(lines[1]).toMatch(/^Last run \d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); // Hero meta 行
174
+ // 尾部「其余页」索引:只列未渲染的两页,命令携带完整 --results 上下文,复制即可复现。
175
+ const tailAt = out.indexOf("Other pages:");
176
+ expect(tailAt).toBeGreaterThan(-1);
177
+ const tail = out.slice(tailAt);
178
+ expect(tail).toContain(`niceeval show --results ${root} --page attempts`);
179
+ expect(tail).toContain(`niceeval show --results ${root} --page traces`);
180
+ expect(tail).not.toContain("--page report"); // 已渲染的首页不进索引
181
+ });
182
+
183
+ it("裸 show 的 --page attempts / traces 渲染内建证据页(AttemptList / TraceWaterfall 的 text 面)", async () => {
184
+ const root = await seedComposedRoot();
185
+ const attempts = await show(root, [], { page: "attempts" }, 160);
186
+ expect(attempts.code).toBe(0);
187
+ // AttemptList text 面:范围内每个 attempt 一条,失败行带主失败摘要。
188
+ expect(attempts.out).toContain("fixtures/button");
189
+ expect(attempts.out).toContain("weather/brooklyn");
190
+ expect(attempts.out).toMatch(/@1[0-9a-z]{7}/);
191
+ const traces = await show(root, [], { page: "traces" }, 160);
192
+ expect(traces.code).toBe(0);
193
+ // TraceWaterfall text 面:每 attempt 一行,行尾是可复制的 --timing 下钻命令。
194
+ expect(traces.out).toMatch(/niceeval show @1[0-9a-z]{7} --timing/);
195
+ expect(traces.out).toContain("no trace"); // fixture 无 trace artifact:如实显示缺失
196
+ });
197
+
198
+ it("裸 show 的选择警告由页内 ScopeWarnings 组件显示(partial-coverage 按动作聚合成组头行)", async () => {
199
+ const root = await makeRoot();
200
+ await writeSnapshot(
201
+ root,
202
+ "2026-07-08T10-00-00-000Z",
203
+ {
204
+ experimentId: "compare/bub",
205
+ startedAt: "2026-07-08T10:00:00.000Z",
206
+ knownEvalIds: ["weather/brooklyn", "weather/queens"], // 少跑一题 → partial-coverage
207
+ },
208
+ [res("weather/brooklyn", "passed")],
209
+ );
210
+ const { out, code } = await show(root, [], {}, 160);
211
+ expect(code).toBe(0);
212
+ // 警告块在 Hero 之后、组内容之前;组头一行含实验 id、徽标与可复制命令。
213
+ expect(out).toMatch(/^! compare\/bub — coverage 1\/2 → niceeval exp compare\/bub$/m);
214
+ expect(out.indexOf("Eval Results")).toBeLessThan(out.indexOf("! compare/bub"));
215
+ });
216
+
166
217
  it("裸 show 的默认报告 chrome 跟随 locale", async () => {
167
218
  const root = await seedComposedRoot();
168
219
  process.env.NICEEVAL_LANG = "zh-CN";
@@ -172,17 +223,24 @@ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text
172
223
  expect(out).toContain("成本 × 端到端成功率 没有可绘制的数据");
173
224
  expect(out).toContain("1 通过 / 1 失败");
174
225
  expect(out).toMatch(/\bbub\s+默认\s+bub\s+1s\s+50%/);
226
+ // 页首 Hero 与尾部页索引同样跟随 locale(内置文案与页名)。
227
+ expect(out.split("\n")[0]).toBe("Eval 运行结果");
228
+ expect(out).toContain("其余页:");
229
+ expect(out).toContain("追踪");
175
230
  } finally {
176
231
  process.env.NICEEVAL_LANG = "en";
177
232
  }
178
233
  });
179
234
 
180
- it("窄终端截断长 eval 与原因,每一行都不超过终端显示宽度", async () => {
235
+ it("窄终端截断长 eval 与原因,报告正文每一行都不超过终端显示宽度", async () => {
181
236
  const root = await seedComposedRoot();
182
237
  const { out, code } = await show(root, [], {}, 60);
183
238
  expect(code).toBe(0);
184
239
  expect(out).toContain("fixtures/button");
185
- for (const line of out.trimEnd().split("\n")) {
240
+ // 「其余页」索引里的命令是可复制的完整命令(携带 --results 绝对路径),复制即可执行,
241
+ // 永不为宽度折行——宽度约束只作用于报告正文。
242
+ const body = out.slice(0, out.indexOf("Other pages:"));
243
+ for (const line of body.trimEnd().split("\n")) {
186
244
  expect(stringWidth(line), line).toBeLessThanOrEqual(60);
187
245
  }
188
246
  });
@@ -438,6 +496,85 @@ describe("--report 装载", () => {
438
496
  return path;
439
497
  }
440
498
 
499
+ /**
500
+ * 两页文件:overview / exam,各自的 text 面输出一个可断言的标记字符串——够验证
501
+ * 「渲染初始页 + 尾部附其余页索引,不倾倒其余页内容」而不需要引入更多组合语义
502
+ * (docs/feature/reports/show/reports.md Case 2)。
503
+ */
504
+ async function writeMultiPageReportFile(dir: string): Promise<string> {
505
+ const path = join(dir, "site.mjs");
506
+ await writeFile(
507
+ path,
508
+ [
509
+ 'const FACES = Symbol.for("niceeval.report.faces");',
510
+ 'const DEFINITION = Symbol.for("niceeval.report.definition");',
511
+ "const Overview = () => null;",
512
+ "Overview[FACES] = { web: () => null, text: () => \"OVERVIEW PAGE CONTENT\" };",
513
+ "const Exam = () => null;",
514
+ "Exam[FACES] = { web: () => null, text: () => \"EXAM PAGE CONTENT\" };",
515
+ "const definition = {",
516
+ ' kind: "report",',
517
+ " links: [],",
518
+ " scripts: [],",
519
+ " styles: [],",
520
+ " pages: [",
521
+ ' { id: "overview", title: { en: "Overview", "zh-CN": "总览" }, content: { type: Overview, props: {} } },',
522
+ ' { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" }, content: { type: Exam, props: {} } },',
523
+ " ],",
524
+ "};",
525
+ "Object.defineProperty(definition, DEFINITION, { value: true });",
526
+ "export default definition;",
527
+ "",
528
+ ].join("\n"),
529
+ "utf-8",
530
+ );
531
+ return path;
532
+ }
533
+
534
+ it("多页文件:渲染初始页(缺省第一页)+ 尾部附其余页索引,不倾倒其余页内容", async () => {
535
+ // show() 测试助手缺省把 root 当 --results 传下去(见上方 show() 定义),
536
+ // 因此索引命令恒含 --results;这里的默认 locale 是 en(beforeAll 固定)。
537
+ const root = await seedComposedRoot();
538
+ const report = await writeMultiPageReportFile(root);
539
+ const { out, code } = await show(root, [], { report });
540
+ expect(code).toBe(0);
541
+ expect(out).toContain("OVERVIEW PAGE CONTENT");
542
+ expect(out).not.toContain("EXAM PAGE CONTENT");
543
+ expect(out).toContain("Other pages:");
544
+ expect(out).toContain(`niceeval show --results ${root} --report ${report} --page exam`);
545
+ expect(out).toContain("Exam");
546
+ expect(out).not.toContain("--page overview"); // 已渲染的页不进「其余页」索引
547
+ });
548
+
549
+ it("多页文件:--page 选中的页渲染,尾部索引只列剩下的页", async () => {
550
+ const root = await seedComposedRoot();
551
+ const report = await writeMultiPageReportFile(root);
552
+ const { out, code } = await show(root, [], { report, page: "exam" });
553
+ expect(code).toBe(0);
554
+ expect(out).toContain("EXAM PAGE CONTENT");
555
+ expect(out).not.toContain("OVERVIEW PAGE CONTENT");
556
+ expect(out).toContain("Other pages:");
557
+ expect(out).toContain(`niceeval show --results ${root} --report ${report} --page overview`);
558
+ expect(out).not.toContain("--page exam");
559
+ });
560
+
561
+ it("单页定义直接渲染,无「其余页」段", async () => {
562
+ const root = await seedComposedRoot();
563
+ const report = await writeReportFile(root);
564
+ const { out, code } = await show(root, [], { report });
565
+ expect(code).toBe(0);
566
+ expect(out).not.toContain("其余页");
567
+ expect(out).not.toContain("Other pages");
568
+ });
569
+
570
+ it("其余页索引命令保留当前 --results / --report 与位置参数上下文,复制即可复现下一层视图", async () => {
571
+ const root = await seedComposedRoot();
572
+ const report = await writeMultiPageReportFile(root);
573
+ const { out, code } = await show(root, ["weather"], { report, results: root });
574
+ expect(code).toBe(0);
575
+ expect(out).toContain(`niceeval show weather --results ${root} --report ${report} --page exam`);
576
+ });
577
+
441
578
  it("装载 + 注入 Selection + attemptCommand 下钻命令", async () => {
442
579
  const root = await seedComposedRoot();
443
580
  const report = await writeReportFile(root);
@@ -521,7 +658,7 @@ describe("--report 装载", () => {
521
658
  const builtin = await show(root, [], { page: "typo" });
522
659
  expect(builtin.code).toBe(1);
523
660
  expect(builtin.err).toContain('page "typo" not found in the built-in report');
524
- expect(builtin.err).toContain("Available pages: report");
661
+ expect(builtin.err).toContain("Available pages: report, attempts, traces");
525
662
  });
526
663
  });
527
664
 
@@ -1,8 +1,9 @@
1
1
  // cases: docs/engineering/unit-tests/reports/cases.md
2
2
  // 「外壳、页面与 Tabs」分区——
3
- // 品牌位恒为 NiceEval 字标(声明 title 也不覆盖)、hero 走标题回退链(与浏览器标题同源;
4
- // document.title 由 useEffect 设置,静态渲染不执行,这里断言 hero 即断言同一个 shellTitle)
5
- // ReportLink.icon 渲染在 label 前(web 面)。契约:docs/feature/reports/library/shell.md「行为约束」。
3
+ // 宿主页头不渲染任何品牌位、宿主无 hero (hero / 品牌行是页内组件,见 site-components 测试);
4
+ // view 导航只有报告定义声明的页(声明序),宿主不追加或保留任何导航项;
5
+ // ReportLink.icon 渲染在 label 前(web 面)。契约:docs/feature/reports/library/shell.md「行为约束」、
6
+ // docs/feature/reports/view.md「页面构成」。
6
7
 
7
8
  import { renderToStaticMarkup } from "react-dom/server";
8
9
  import { beforeAll, describe, expect, it } from "vitest";
@@ -14,14 +15,18 @@ beforeAll(() => {
14
15
  (globalThis as { location?: unknown }).location = { hash: "", search: "", pathname: "/" };
15
16
  });
16
17
 
17
- const reportPages = { report: { en: "<p>REPORT_BODY</p>", "zh-CN": "<p>REPORT_BODY</p>" } };
18
+ const reportPages = {
19
+ report: { en: "<p>REPORT_BODY</p>", "zh-CN": "<p>REPORT_BODY</p>" },
20
+ attempts: { en: "<p>ATTEMPTS_BODY</p>", "zh-CN": "<p>ATTEMPTS_BODY</p>" },
21
+ traces: { en: "<p>TRACES_BODY</p>", "zh-CN": "<p>TRACES_BODY</p>" },
22
+ };
18
23
 
19
24
  function dataWithShell(report: ViewData["report"]): ViewData {
20
25
  return { composedRuns: 1, snapshots: [], ...(report !== undefined ? { report } : {}) };
21
26
  }
22
27
 
23
- describe("外壳:品牌位、hero 标题与 ReportLink.icon", () => {
24
- it("声明 title 后品牌位仍是 NiceEval 字标;hero 显示走完回退链的报告标题", () => {
28
+ describe("外壳:宿主无品牌位与 hero,导航只有报告页", () => {
29
+ it("宿主导航壳 DOM 无品牌节点、无 hero 区:品牌与 hero 是页内组件,不归宿主", () => {
25
30
  const html = renderToStaticMarkup(
26
31
  <App
27
32
  data={dataWithShell({
@@ -33,19 +38,75 @@ describe("外壳:品牌位、hero 标题与 ReportLink.icon", () => {
33
38
  reportPages={reportPages}
34
39
  />,
35
40
  );
36
- // 品牌位:恒定的 NiceEval 字标,不吃报告 title。
37
- const brand = html.match(/class="brand"[\s\S]*?<\/a>/)![0];
38
- expect(brand).toContain(">NiceEval</span>");
39
- expect(brand).not.toContain("Memory Evals");
40
- // hero:报告 title(node 环境 locale 回退 en)
41
- const hero = html.match(/<h1>[\s\S]*?<\/h1>/)![0];
42
- expect(hero).toContain("Memory Evals");
41
+ // 宿主 DOM 无品牌节点:没有字标、没有 Powered by、没有任何官网品牌链接。
42
+ expect(html).not.toContain('class="brand"');
43
+ expect(html).not.toContain("NiceEval");
44
+ expect(html).not.toContain("Powered by");
45
+ expect(html).not.toContain("niceeval.com");
46
+ // 宿主无 hero 区:标题只落浏览器 <title>(useEffect,静态渲染不执行),不落任何宿主节点。
47
+ expect(html).not.toContain('class="hero"');
48
+ expect(html).not.toContain("<h1");
49
+ expect(html).not.toContain("Memory Evals");
43
50
  });
44
51
 
45
- it("缺外壳声明(旧数据)时 hero 落内置文案 Eval Results,品牌位不变", () => {
46
- const html = renderToStaticMarkup(<App data={dataWithShell(undefined)} reportPages={reportPages} />);
47
- expect(html.match(/<h1>[\s\S]*?<\/h1>/)![0]).toContain("Eval Results");
48
- expect(html.match(/class="brand"[\s\S]*?<\/a>/)![0]).toContain(">NiceEval</span>");
52
+ it("导航项 = 报告定义声明的页,按声明序;宿主不追加 Attempts / Traces 等任何项", () => {
53
+ const html = renderToStaticMarkup(
54
+ <App
55
+ data={dataWithShell({
56
+ title: "T",
57
+ links: [],
58
+ pages: [
59
+ { id: "overview", title: { en: "Overview", "zh-CN": "总览" } },
60
+ { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" } },
61
+ ],
62
+ initialPageId: "overview",
63
+ })}
64
+ reportPages={{
65
+ overview: { en: "<p>A</p>", "zh-CN": "<p>A</p>" },
66
+ exam: { en: "<p>B</p>", "zh-CN": "<p>B</p>" },
67
+ }}
68
+ />,
69
+ );
70
+ const triggers = html.match(/role="tab"/g) ?? [];
71
+ expect(triggers).toHaveLength(2); // 恰为声明的两页,无宿主追加项
72
+ expect(html.indexOf("Overview")).toBeLessThan(html.indexOf("Exam")); // 声明序
73
+ expect(html).not.toContain("#/attempts");
74
+ expect(html).not.toContain("#/traces");
75
+ });
76
+
77
+ it("裸 view(内建报告三页声明)导航恰为 报告 · Attempts · 追踪,来自页列表而非宿主", () => {
78
+ const html = renderToStaticMarkup(
79
+ <App
80
+ data={dataWithShell({
81
+ title: { en: "Eval Results", "zh-CN": "Eval 运行结果" },
82
+ links: [],
83
+ pages: [
84
+ { id: "report", title: { en: "Report", "zh-CN": "报告" } },
85
+ { id: "attempts", title: "Attempts" },
86
+ { id: "traces", title: { en: "Traces", "zh-CN": "追踪" } },
87
+ ],
88
+ initialPageId: "report",
89
+ })}
90
+ reportPages={reportPages}
91
+ />,
92
+ );
93
+ expect(html.match(/role="tab"/g)).toHaveLength(3);
94
+ for (const label of ["Report", "Attempts", "Traces"]) expect(html).toContain(label);
95
+ });
96
+
97
+ it("树形态定义(单页 report)导航只有一项", () => {
98
+ const html = renderToStaticMarkup(
99
+ <App
100
+ data={dataWithShell({
101
+ title: "T",
102
+ links: [],
103
+ pages: [{ id: "report", title: { en: "Report", "zh-CN": "报告" } }],
104
+ initialPageId: "report",
105
+ })}
106
+ reportPages={{ report: reportPages.report }}
107
+ />,
108
+ );
109
+ expect(html.match(/role="tab"/g)).toHaveLength(1);
49
110
  });
50
111
 
51
112
  it("ReportLink.icon 的内联 SVG 渲染在 label 前,原样内联", () => {
@@ -1,27 +1,17 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { detectLocale, makeTranslator, persistLocale, setDocumentLocale } from "./i18n.ts";
3
3
  import type { Locale, LocalizedText, ReportSlotHtml, Tab, ViewData, ViewReportPageMeta, ViewResult } from "./types.ts";
4
- import { flattenAttempts, resultFromUrl } from "./lib/rows.ts";
4
+ import { resultFromUrl } from "./lib/rows.ts";
5
5
  import { parseAttemptHash, resolveAttemptLocator, unresolvedAttemptWarning } from "./lib/attempt-route.ts";
6
- import { formatDateTime } from "./lib/format.ts";
7
- import { CopyFixPrompt } from "./components/CopyControls.tsx";
8
- import { SkippedRunsBanner } from "./components/SkippedRunsBanner.tsx";
9
6
  import { AttemptModal } from "./components/AttemptModal.tsx";
10
7
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.tsx";
11
- import { AttemptsView } from "./pages/AttemptsPage.tsx";
12
- import { TracesView } from "./pages/TracesPage.tsx";
13
8
 
14
- // 导航组成固定(docs/feature/reports/view.md「页面构成」):报告页按声明顺序在前
15
- // (路由 `#/page/<id>`,`--page <id>` 定初始页),内置的 Attempts、Traces 证据页恒排在
16
- // 报告页之后——证据页由宿主拥有,报告定义不能移除或重排它们。
17
- // 报告页的 tab 值带 `page:` 前缀,避免与证据页 id(attempts / traces)撞名。
18
- const EVIDENCE_TABS: { id: Tab; label: "nav.attempts" | "nav.traces" }[] = [
19
- { id: "attempts", label: "nav.attempts" },
20
- { id: "traces", label: "nav.traces" },
21
- ];
22
-
23
- /** niceeval 官网;web 面 hero 下方恒含指向它的 `Powered by NiceEval` 一行,无关闭配置。 */
24
- const NICEEVAL_SITE_URL = "https://niceeval.com";
9
+ // 导航组成只有一条规则(docs/feature/reports/view.md「页面构成」):导航项 = 报告定义声明的页,
10
+ // 按声明顺序排列(路由 `#/page/<id>`,`--page <id>` 定初始页)。宿主不追加、不保留任何导航项——
11
+ // 裸 view 的「报告 / Attempts / 追踪」三个 tab 就是内建报告的三页。页面里的 hero、品牌行、
12
+ // 选择警告都不是宿主渲染的:它们是页内的站点组件(Hero / PoweredBy / ScopeWarnings)
13
+ // 宿主保留的只有机器:管线与路由、attempt 详情路由、文档单例(<title>)、语言切换
14
+ // (docs/feature/reports/architecture.md「宿主保留的只有机器」)。
25
15
 
26
16
  /**
27
17
  * LocalizedText 的确定回退(docs/feature/reports/library/shell.md):当前 locale → en →
@@ -61,23 +51,20 @@ function ReportSlot({ html }: { html: string }) {
61
51
  return <div className="report-slot" dangerouslySetInnerHTML={markup} />;
62
52
  }
63
53
 
64
- /** `#/page/<id>` / `#/attempts` / `#/traces` → tab 值;认不出返回 null(交给初始页兜底)。 */
54
+ /** `#/page/<id>` → tab 值;认不出返回 null(交给初始页兜底)。 */
65
55
  function tabFromHash(hash: string, pages: ViewReportPageMeta[]): Tab | null {
66
56
  const pageMatch = /^#\/page\/([a-z0-9-]+)$/.exec(hash);
67
57
  if (pageMatch && pages.some((p) => p.id === pageMatch[1])) return `page:${pageMatch[1]}`;
68
- if (hash === "#/attempts") return "attempts";
69
- if (hash === "#/traces") return "traces";
70
58
  return null;
71
59
  }
72
60
 
73
- /** tab 值 → hash 路由(报告页 `#/page/<id>`,证据页 `#/attempts` / `#/traces`)。 */
61
+ /** tab 值 → hash 路由(报告页 `#/page/<id>`)。 */
74
62
  function hashForTab(tab: Tab): string {
75
- return tab.startsWith("page:") ? `#/page/${tab.slice("page:".length)}` : `#/${tab}`;
63
+ return `#/page/${tab.slice("page:".length)}`;
76
64
  }
77
65
 
78
66
  export function App({ data, reportPages }: { data: ViewData; reportPages: Record<string, ReportSlotHtml> }) {
79
67
  const snapshots = data.snapshots ?? [];
80
- const attempts = useMemo(() => flattenAttempts(snapshots), [snapshots]);
81
68
  const [locale, setLocale] = useState<Locale>(() => detectLocale());
82
69
  const t = useMemo(() => makeTranslator(locale), [locale]);
83
70
 
@@ -99,9 +86,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
99
86
  persistLocale(locale);
100
87
  }, [locale]);
101
88
 
102
- // 首页 hero 与浏览器标题跟随外壳标题(回退链在 server 侧走完:def.title → 唯一快照 name →
103
- // 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
104
- // 页头品牌位不归它——那里是恒定的 NiceEval 字标。
89
+ // 浏览器标题是宿主文档单例:跟随外壳标题(回退链在 server 侧走完:def.title →
90
+ // 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
91
+ // 页面里的 hero 标题不归宿主——它是页内 Hero 组件,同一取值链经 ctx.report.title 贯通。
105
92
  const shellTitle = localizedText(data.report?.title, locale) ?? t("hero.title");
106
93
  useEffect(() => {
107
94
  document.title = shellTitle;
@@ -123,7 +110,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
123
110
  }, []);
124
111
 
125
112
  // 浏览器前进/后退、手改 hash、页内链接(attempt 深链与 `#/page/<id>` 页路由)统一从
126
- // hashchange 分发:attempt hash 开证据室弹窗,页/证据室 hash 切当前 tab。
113
+ // hashchange 分发:attempt hash 开证据室弹窗,页 hash 切当前 tab。
114
+ // attempt 详情路由对完整结果根解析(viewData.snapshots 全量通道):被位置参数 / --experiment
115
+ // 收窄滤掉的 attempt 仍能经深链打开,报告里的证据引用不因页面过滤失效。
127
116
  useEffect(() => {
128
117
  const onHashChange = () => {
129
118
  const locator = parseAttemptHash(location.hash);
@@ -162,23 +151,12 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
162
151
  return (
163
152
  <Tabs value={tab} onValueChange={(v) => selectTab(v as Tab)}>
164
153
  <header className="topbar">
165
- {/* 页头左端是恒定的 NiceEval 品牌字标(与 Powered by 行同族的产品品牌位),
166
- 报告定义不能覆盖或移除;报告 title 的落点是下方 hero 与浏览器标题。 */}
167
- <a className="brand" href={hashForTab(`page:${initialPageId}`)}>
168
- <span className="mark" />
169
- <span>NiceEval</span>
170
- </a>
171
154
  <TabsList aria-label={t("nav.label")}>
172
155
  {pages.map((page) => (
173
156
  <TabsTrigger key={`page:${page.id}`} value={`page:${page.id}`}>
174
157
  {localizedText(page.title, locale) ?? page.id}
175
158
  </TabsTrigger>
176
159
  ))}
177
- {EVIDENCE_TABS.map((item) => (
178
- <TabsTrigger key={item.id} value={item.id}>
179
- {t(item.label)}
180
- </TabsTrigger>
181
- ))}
182
160
  </TabsList>
183
161
  {data.report?.links?.length ? (
184
162
  <nav className="shell-links" aria-label="Links">
@@ -208,53 +186,14 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
208
186
  </div>
209
187
  </header>
210
188
  <main>
211
- <section className="hero">
212
- {/* hero 标题 = 走完回退链的报告标题(与浏览器标题同源)。 */}
213
- <h1>{shellTitle}</h1>
214
- <div className="meta">
215
- <span>
216
- {/* viewData 只带原始值(ISO / number),这里按当前界面 locale 格式化。 */}
217
- <b>{t("hero.lastRun")}</b> {data.lastRunAt ? formatDateTime(data.lastRunAt, locale) : t("hero.noRuns")}
218
- </span>
219
- {data.composedRuns > 0 ? (
220
- // 报告槽是跨 run 合成的现刻水位,hero 如实标注合成来源(几个 run)。
221
- <span>{t("hero.composedFrom", { count: data.composedRuns })}</span>
222
- ) : null}
223
- </div>
224
- {/* 品牌行:恒在 hero 之下、恒带官网链接,不占 footer 的语义位、没有关闭配置
225
- (shell.md「行为约束」)。 */}
226
- <span className="powered-by">
227
- <a href={NICEEVAL_SITE_URL} target="_blank" rel="noreferrer">
228
- Powered by NiceEval
229
- </a>
230
- </span>
231
- </section>
232
-
233
- <SkippedRunsBanner skippedRuns={data.skippedRuns ?? []} t={t} />
234
-
235
189
  {pages.map((page) => (
236
190
  <TabsContent key={`page:${page.id}`} value={`page:${page.id}`} id={`tab-page-${page.id}`}>
237
- {/* 壳区:报告槽上方靠右的批量修复 prompt 按钮。失败清单从 viewData.snapshots
238
- 现算(latest 口径),默认报告与 --report 两种填充下都在。 */}
239
- <div className="section-sub-head">
240
- <span className="group-detail-label" />
241
- <div className="controls">
242
- <CopyFixPrompt snapshots={snapshots} t={t} />
243
- </div>
244
- </div>
245
191
  {/* 报告槽:server 侧逐页渲染好的静态 HTML(含 <Style> 产物),按当前页与界面语言
246
- 摆放对应块;Scope 警告由报告页内呈现,壳不设第二条通道。
192
+ 摆放对应块;hero、品牌行、Scope 警告、批量修复 prompt 都是页内组件,壳不再渲染。
247
193
  attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
248
194
  <ReportSlot html={reportPages[page.id]?.[locale] || reportPages[page.id]?.en || ""} />
249
195
  </TabsContent>
250
196
  ))}
251
-
252
- <TabsContent value="attempts">
253
- <AttemptsView attempts={attempts} t={t} />
254
- </TabsContent>
255
- <TabsContent value="traces">
256
- <TracesView attempts={attempts} t={t} />
257
- </TabsContent>
258
197
  </main>
259
198
  {footerText ? (
260
199
  <footer className="site-footer">
@@ -1,8 +1,7 @@
1
1
  import React, { useState } from "react";
2
2
  import { Check, Copy } from "lucide-react";
3
3
  import type { T } from "../shared.ts";
4
- import type { ViewResult, ViewSnapshot } from "../types.ts";
5
- import { snapshotLabel } from "../lib/rows.ts";
4
+ import type { ViewResult } from "../types.ts";
6
5
  import { reasonFor } from "../lib/verdict.ts";
7
6
 
8
7
  /** 修复 prompt 的一条失败条目;路径均相对 view 输入根(默认 `.niceeval/`)。 */
@@ -63,46 +62,9 @@ export function buildFixPrompt(entries: FixPromptEntry[]): string {
63
62
  ].join("\n");
64
63
  }
65
64
 
66
- /**
67
- * 报告槽同款口径的失败清单:每个 experiment 最新一次快照(latest 标记;快照明细已在
68
- * server 侧跨快照去重)里的 failed / errored attempt,从 viewData.snapshots 现算——
69
- * 默认报告与 --report 两种填充下按钮都在,不依赖任何统计产物。
70
- */
71
- export function fixPromptEntries(snapshots: ViewSnapshot[]): FixPromptEntry[] {
72
- return snapshots
73
- .filter((s) => s.latest)
74
- .flatMap((snapshot) =>
75
- snapshot.results
76
- .filter((r: ViewResult) => r.verdict === "failed" || r.verdict === "errored")
77
- .map((r: ViewResult) => toFixPromptEntry(r, snapshotLabel(snapshot))),
78
- );
79
- }
80
-
81
- export function CopyFixPrompt({ snapshots, t }: { snapshots: ViewSnapshot[]; t: T }) {
82
- const [copied, setCopied] = useState(false);
83
-
84
- const entries = fixPromptEntries(snapshots);
85
-
86
- if (!entries.length) return null;
87
-
88
- const copy = async (event: React.MouseEvent<HTMLButtonElement>) => {
89
- event.stopPropagation();
90
- try {
91
- await copyText(buildFixPrompt(entries));
92
- setCopied(true);
93
- setTimeout(() => setCopied(false), 1500);
94
- } catch {
95
- setCopied(false);
96
- }
97
- };
98
-
99
- return (
100
- <button className={`copy-all-errors${copied ? " is-copied" : ""}`} onClick={copy} title={t("action.copyPrompt")}>
101
- {copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
102
- <span>{copied ? t("action.copied") : `${t("action.copyPrompt")} (${entries.length})`}</span>
103
- </button>
104
- );
105
- }
65
+ // 批量修复 prompt 不再由壳渲染:它是内建报告首页里的 CopyFixPrompt 组件
66
+ // (niceeval/report 的站点组件,prompt resolve 期烘进静态 HTML)。这里只留
67
+ // attempt 弹窗的单条版。
106
68
 
107
69
  /** attempt 弹窗里的单条版:只打包当前 attempt 的失败,供逐条转交 agent。 */
108
70
  export function CopyAttemptPrompt({ result, t }: { result: ViewResult; t: T }) {