niceeval 0.8.0 → 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 (143) 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 +72 -6
  10. package/dist/report/components.js +159 -10
  11. package/dist/report/compute.d.ts +31 -4
  12. package/dist/report/compute.js +131 -12
  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 -10
  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 -11
  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 +6 -8
  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 -2
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/docs-site/zh/reference/report-components.mdx +2 -2
  59. package/docs-site/zh/reference/results-data.mdx +2 -4
  60. package/package.json +2 -1
  61. package/src/agents/bub.ts +13 -1
  62. package/src/agents/claude-code.test.ts +43 -1
  63. package/src/agents/claude-code.ts +32 -14
  64. package/src/agents/codex.test.ts +168 -1
  65. package/src/agents/codex.ts +51 -15
  66. package/src/agents/mcp.ts +31 -0
  67. package/src/agents/post-setup.ts +33 -0
  68. package/src/agents/types.ts +28 -7
  69. package/src/cli.ts +9 -11
  70. package/src/context/context.ts +11 -5
  71. package/src/define.ts +3 -0
  72. package/src/i18n/en.ts +5 -2
  73. package/src/i18n/zh-CN.ts +5 -1
  74. package/src/index.ts +1 -0
  75. package/src/report/built-in/index.tsx +8 -7
  76. package/src/report/built-in/standard.tsx +59 -0
  77. package/src/report/components.tsx +231 -17
  78. package/src/report/compute.ts +146 -21
  79. package/src/report/dual-render.test.tsx +139 -12
  80. package/src/report/index.ts +20 -1
  81. package/src/report/locale.ts +83 -1
  82. package/src/report/react/AttemptList.tsx +13 -1
  83. package/src/report/react/CopyFixPrompt.tsx +37 -0
  84. package/src/report/react/HeroCard.tsx +59 -0
  85. package/src/report/react/PoweredBy.tsx +20 -0
  86. package/src/report/react/ScopeWarnings.tsx +74 -0
  87. package/src/report/react/TraceWaterfall.tsx +78 -0
  88. package/src/report/react/enhance.js +14 -0
  89. package/src/report/react/index.tsx +11 -0
  90. package/src/report/react/styles.css +187 -7
  91. package/src/report/report.test.ts +2 -20
  92. package/src/report/report.ts +97 -62
  93. package/src/report/scope-warnings.ts +155 -0
  94. package/src/report/site-components.test.tsx +526 -0
  95. package/src/report/text/faces.ts +66 -0
  96. package/src/report/tree.ts +8 -1
  97. package/src/report/types.ts +51 -11
  98. package/src/report/web.ts +7 -40
  99. package/src/results/copy.ts +15 -78
  100. package/src/results/host-equivalence.test.ts +5 -1
  101. package/src/results/open.ts +5 -4
  102. package/src/results/publish.ts +4 -146
  103. package/src/results/results.test.ts +86 -9
  104. package/src/results/select.ts +78 -10
  105. package/src/results/types.ts +27 -7
  106. package/src/runner/attempt.ts +10 -9
  107. package/src/runner/discover.test.ts +9 -1
  108. package/src/runner/discover.ts +3 -3
  109. package/src/runner/fingerprint.ts +9 -4
  110. package/src/runner/ledger.test.ts +30 -1
  111. package/src/runner/ledger.ts +26 -4
  112. package/src/runner/run.ts +5 -1
  113. package/src/runner/sandbox-selection.test.ts +131 -0
  114. package/src/runner/sandbox-selection.ts +110 -0
  115. package/src/runner/types.ts +19 -2
  116. package/src/sandbox/types.ts +6 -0
  117. package/src/show/index.ts +17 -10
  118. package/src/show/render.ts +11 -11
  119. package/src/show/report-host.test.ts +32 -15
  120. package/src/show/report-host.ts +5 -4
  121. package/src/show/show.test.ts +140 -3
  122. package/src/view/app/App.test.tsx +78 -17
  123. package/src/view/app/App.tsx +17 -78
  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/artifact-serving.test.ts +1 -1
  130. package/src/view/client-dist/app.css +1 -1
  131. package/src/view/client-dist/app.js +20 -20
  132. package/src/view/data.ts +2 -13
  133. package/src/view/index.ts +1 -12
  134. package/src/view/server.ts +0 -2
  135. package/src/view/shared/types.ts +11 -6
  136. package/src/view/site-parity.test.ts +1 -1
  137. package/src/view/site.ts +1 -1
  138. package/src/view/styles.css +6 -266
  139. package/src/view/view-report.test.ts +64 -27
  140. package/src/view/app/components/LazyArtifact.tsx +0 -51
  141. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  142. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  143. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -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 }) {