niceeval 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +3 -1
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +69 -2
  10. package/dist/report/components.js +152 -3
  11. package/dist/report/compute.d.ts +28 -1
  12. package/dist/report/compute.js +123 -0
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +68 -55
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.d.ts +1 -1
  38. package/dist/report/tree.js +7 -2
  39. package/dist/report/types.d.ts +45 -0
  40. package/dist/report/web.d.ts +5 -4
  41. package/dist/report/web.js +7 -22
  42. package/dist/results/select.d.ts +17 -4
  43. package/dist/results/select.js +76 -11
  44. package/dist/results/types.d.ts +26 -0
  45. package/dist/runner/fingerprint.d.ts +3 -3
  46. package/dist/runner/sandbox-selection.d.ts +12 -0
  47. package/dist/runner/types.d.ts +18 -6
  48. package/dist/sandbox/types.d.ts +12 -0
  49. package/dist/shared/aggregate.d.ts +1 -1
  50. package/dist/shared/aggregate.js +1 -1
  51. package/docs-site/zh/explanation/evals.mdx +2 -1
  52. package/docs-site/zh/explanation/experiment.mdx +2 -0
  53. package/docs-site/zh/how-to/custom-reports.mdx +23 -7
  54. package/docs-site/zh/how-to/experiments.mdx +2 -2
  55. package/docs-site/zh/how-to/publish-report.mdx +11 -4
  56. package/docs-site/zh/how-to/viewing-results.mdx +17 -8
  57. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  58. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  59. package/docs-site/zh/reference/cli.mdx +3 -3
  60. package/docs-site/zh/reference/define-eval.mdx +8 -0
  61. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  62. package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
  63. package/package.json +2 -1
  64. package/src/agents/bub.ts +13 -1
  65. package/src/agents/claude-code.test.ts +43 -1
  66. package/src/agents/claude-code.ts +32 -14
  67. package/src/agents/codex.test.ts +168 -1
  68. package/src/agents/codex.ts +51 -15
  69. package/src/agents/mcp.ts +31 -0
  70. package/src/agents/post-setup.ts +33 -0
  71. package/src/agents/types.ts +28 -7
  72. package/src/cli.ts +15 -14
  73. package/src/define.ts +3 -0
  74. package/src/i18n/en.ts +8 -5
  75. package/src/i18n/zh-CN.ts +8 -4
  76. package/src/index.ts +1 -0
  77. package/src/report/built-in/index.tsx +8 -7
  78. package/src/report/built-in/standard.tsx +59 -0
  79. package/src/report/components.tsx +218 -2
  80. package/src/report/compute.ts +138 -1
  81. package/src/report/dual-render.test.tsx +141 -14
  82. package/src/report/index.ts +20 -0
  83. package/src/report/locale.ts +83 -1
  84. package/src/report/react/AttemptList.tsx +13 -1
  85. package/src/report/react/CopyFixPrompt.tsx +37 -0
  86. package/src/report/react/HeroCard.tsx +59 -0
  87. package/src/report/react/PoweredBy.tsx +20 -0
  88. package/src/report/react/ScopeWarnings.tsx +74 -0
  89. package/src/report/react/TraceWaterfall.tsx +78 -0
  90. package/src/report/react/enhance.js +14 -0
  91. package/src/report/react/index.tsx +11 -0
  92. package/src/report/react/styles.css +193 -7
  93. package/src/report/report.ts +99 -64
  94. package/src/report/scope-warnings.ts +155 -0
  95. package/src/report/site-components.test.tsx +526 -0
  96. package/src/report/text/faces.ts +66 -0
  97. package/src/report/tree.ts +10 -3
  98. package/src/report/types.ts +51 -0
  99. package/src/report/web.ts +7 -40
  100. package/src/results/host-equivalence.test.ts +6 -2
  101. package/src/results/open.ts +5 -4
  102. package/src/results/results.test.ts +78 -1
  103. package/src/results/select.ts +80 -12
  104. package/src/results/types.ts +27 -0
  105. package/src/runner/attempt.ts +10 -9
  106. package/src/runner/discover.test.ts +9 -1
  107. package/src/runner/discover.ts +3 -3
  108. package/src/runner/fingerprint.ts +9 -4
  109. package/src/runner/ledger.test.ts +30 -1
  110. package/src/runner/ledger.ts +26 -4
  111. package/src/runner/run.ts +5 -1
  112. package/src/runner/sandbox-selection.test.ts +131 -0
  113. package/src/runner/sandbox-selection.ts +110 -0
  114. package/src/runner/types.ts +19 -2
  115. package/src/sandbox/types.ts +6 -0
  116. package/src/shared/aggregate.ts +1 -1
  117. package/src/show/index.ts +19 -12
  118. package/src/show/render.ts +12 -12
  119. package/src/show/report-host.test.ts +34 -17
  120. package/src/show/report-host.ts +6 -5
  121. package/src/show/show.test.ts +141 -4
  122. package/src/view/app/App.test.tsx +79 -17
  123. package/src/view/app/App.tsx +25 -74
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/client-dist/app.css +1 -1
  130. package/src/view/client-dist/app.js +20 -20
  131. package/src/view/data.ts +21 -10
  132. package/src/view/index.ts +2 -12
  133. package/src/view/server.ts +4 -4
  134. package/src/view/shared/types.ts +11 -6
  135. package/src/view/styles.css +9 -252
  136. package/src/view/view-report.test.ts +99 -33
  137. package/src/view/app/components/LazyArtifact.tsx +0 -51
  138. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  139. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  140. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -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,34 +171,35 @@ 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
  });
182
199
 
183
- it("showCommand 按序携带位置参数与 --experiment / --results / --report / --page", () => {
200
+ it("showCommand 按序携带位置参数与 --exp / --results / --report / --page", () => {
184
201
  expect(
185
202
  showCommand({ patterns: ["memory/swelancer"], experiment: "dev-e2b", report: "reports/site.tsx", page: "exam" }),
186
- ).toBe("niceeval show memory/swelancer --experiment dev-e2b --report reports/site.tsx --page exam");
203
+ ).toBe("niceeval show memory/swelancer --exp dev-e2b --report reports/site.tsx --page exam");
187
204
  });
188
205
  });
@@ -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
@@ -319,7 +320,7 @@ export interface HostCommandContext {
319
320
  /** 按上下文拼一条可复制的 show 命令(页索引 / 组索引共用的携带规则)。 */
320
321
  export function showCommand(ctx: HostCommandContext, extra: string[] = []): string {
321
322
  const parts = ["niceeval show", ...ctx.patterns];
322
- if (ctx.experiment !== undefined) parts.push(`--experiment ${ctx.experiment}`);
323
+ if (ctx.experiment !== undefined) parts.push(`--exp ${ctx.experiment}`);
323
324
  if (ctx.results !== undefined) parts.push(`--results ${ctx.results}`);
324
325
  if (ctx.report !== undefined) parts.push(`--report ${ctx.report}`);
325
326
  if (ctx.page !== undefined) parts.push(`--page ${ctx.page}`);
@@ -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);
@@ -455,7 +592,7 @@ describe("--report 装载", () => {
455
592
  expect(out).toContain("CUSTOM weather/brooklyn ·");
456
593
  });
457
594
 
458
- it("--experiment 让 Selection 只留该实验", async () => {
595
+ it("--exp 让 Selection 只留该实验", async () => {
459
596
  const root = await seedComposedRoot();
460
597
  await writeSnapshot(
461
598
  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("外壳:宿主恒有 NiceEval 品牌位、无 hero,导航只有报告页", () => {
29
+ it("宿主页头有恒定品牌字标、无 hero 区:hero 是页内组件不归宿主,品牌归宿主", () => {
25
30
  const html = renderToStaticMarkup(
26
31
  <App
27
32
  data={dataWithShell({
@@ -33,19 +38,76 @@ 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
+ // 宿主页头恒有 NiceEval 品牌字标,外链官网;报告定义不能覆盖或移除。
42
+ expect(html).toContain('class="brand"');
43
+ expect(html).toContain("NiceEval");
44
+ expect(html).toContain("niceeval.com");
45
+ // 品牌位是页头字标,不是 hero Powered by 行——后者是页内组件,不落宿主壳。
46
+ expect(html).not.toContain("Powered by");
47
+ // 宿主无 hero 区:标题只落浏览器 <title>(useEffect,静态渲染不执行),不落任何宿主节点。
48
+ expect(html).not.toContain('class="hero"');
49
+ expect(html).not.toContain("<h1");
50
+ expect(html).not.toContain("Memory Evals");
43
51
  });
44
52
 
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>");
53
+ it("导航项 = 报告定义声明的页,按声明序;宿主不追加 Attempts / Traces 等任何项", () => {
54
+ const html = renderToStaticMarkup(
55
+ <App
56
+ data={dataWithShell({
57
+ title: "T",
58
+ links: [],
59
+ pages: [
60
+ { id: "overview", title: { en: "Overview", "zh-CN": "总览" } },
61
+ { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" } },
62
+ ],
63
+ initialPageId: "overview",
64
+ })}
65
+ reportPages={{
66
+ overview: { en: "<p>A</p>", "zh-CN": "<p>A</p>" },
67
+ exam: { en: "<p>B</p>", "zh-CN": "<p>B</p>" },
68
+ }}
69
+ />,
70
+ );
71
+ const triggers = html.match(/role="tab"/g) ?? [];
72
+ expect(triggers).toHaveLength(2); // 恰为声明的两页,无宿主追加项
73
+ expect(html.indexOf("Overview")).toBeLessThan(html.indexOf("Exam")); // 声明序
74
+ expect(html).not.toContain("#/attempts");
75
+ expect(html).not.toContain("#/traces");
76
+ });
77
+
78
+ it("裸 view(内建报告三页声明)导航恰为 报告 · Attempts · 追踪,来自页列表而非宿主", () => {
79
+ const html = renderToStaticMarkup(
80
+ <App
81
+ data={dataWithShell({
82
+ title: { en: "Eval Results", "zh-CN": "Eval 运行结果" },
83
+ links: [],
84
+ pages: [
85
+ { id: "report", title: { en: "Report", "zh-CN": "报告" } },
86
+ { id: "attempts", title: "Attempts" },
87
+ { id: "traces", title: { en: "Traces", "zh-CN": "追踪" } },
88
+ ],
89
+ initialPageId: "report",
90
+ })}
91
+ reportPages={reportPages}
92
+ />,
93
+ );
94
+ expect(html.match(/role="tab"/g)).toHaveLength(3);
95
+ for (const label of ["Report", "Attempts", "Traces"]) expect(html).toContain(label);
96
+ });
97
+
98
+ it("树形态定义(单页 report)导航只有一项", () => {
99
+ const html = renderToStaticMarkup(
100
+ <App
101
+ data={dataWithShell({
102
+ title: "T",
103
+ links: [],
104
+ pages: [{ id: "report", title: { en: "Report", "zh-CN": "报告" } }],
105
+ initialPageId: "report",
106
+ })}
107
+ reportPages={{ report: reportPages.report }}
108
+ />,
109
+ );
110
+ expect(html.match(/role="tab"/g)).toHaveLength(1);
49
111
  });
50
112
 
51
113
  it("ReportLink.icon 的内联 SVG 渲染在 label 前,原样内联", () => {
@@ -1,27 +1,21 @@
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
- ];
9
+ // 导航组成只有一条规则(docs/feature/reports/view.md「页面构成」):导航项 = 报告定义声明的页,
10
+ // 按声明顺序排列(路由 `#/page/<id>`,`--page <id>` 定初始页)。宿主不追加、不保留任何导航项——
11
+ // 裸 view 的「报告 / Attempts / 追踪」三个 tab 就是内建报告的三页。页面里的 hero、Scope 警告
12
+ // 都不是宿主渲染的:它们是页内的站点组件(Hero / ScopeWarnings)。宿主保留的是机器
13
+ // 加一个恒定的品牌位:管线与路由、attempt 详情路由、文档单例(<title>)、语言切换,以及
14
+ // 页头左端的 NiceEval 字标(docs/feature/reports/architecture.md「宿主保留的只有机器」)。
22
15
 
23
- /** niceeval 官网;web hero 下方恒含指向它的 `Powered by NiceEval` 一行,无关闭配置。 */
24
- const NICEEVAL_SITE_URL = "https://niceeval.com";
16
+ // niceeval 官网。页头品牌字标与 hero 下的 `Powered by NiceEval` 行都外链到它,
17
+ // utm_medium 区分点击来自哪个品牌位(shell.md「行为约束」)。
18
+ const BRAND_HREF = "https://niceeval.com/?utm_source=report&utm_medium=brand";
25
19
 
26
20
  /**
27
21
  * LocalizedText 的确定回退(docs/feature/reports/library/shell.md):当前 locale → en →
@@ -61,23 +55,20 @@ function ReportSlot({ html }: { html: string }) {
61
55
  return <div className="report-slot" dangerouslySetInnerHTML={markup} />;
62
56
  }
63
57
 
64
- /** `#/page/<id>` / `#/attempts` / `#/traces` → tab 值;认不出返回 null(交给初始页兜底)。 */
58
+ /** `#/page/<id>` → tab 值;认不出返回 null(交给初始页兜底)。 */
65
59
  function tabFromHash(hash: string, pages: ViewReportPageMeta[]): Tab | null {
66
60
  const pageMatch = /^#\/page\/([a-z0-9-]+)$/.exec(hash);
67
61
  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
62
  return null;
71
63
  }
72
64
 
73
- /** tab 值 → hash 路由(报告页 `#/page/<id>`,证据页 `#/attempts` / `#/traces`)。 */
65
+ /** tab 值 → hash 路由(报告页 `#/page/<id>`)。 */
74
66
  function hashForTab(tab: Tab): string {
75
- return tab.startsWith("page:") ? `#/page/${tab.slice("page:".length)}` : `#/${tab}`;
67
+ return `#/page/${tab.slice("page:".length)}`;
76
68
  }
77
69
 
78
70
  export function App({ data, reportPages }: { data: ViewData; reportPages: Record<string, ReportSlotHtml> }) {
79
71
  const snapshots = data.snapshots ?? [];
80
- const attempts = useMemo(() => flattenAttempts(snapshots), [snapshots]);
81
72
  const [locale, setLocale] = useState<Locale>(() => detectLocale());
82
73
  const t = useMemo(() => makeTranslator(locale), [locale]);
83
74
 
@@ -99,9 +90,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
99
90
  persistLocale(locale);
100
91
  }, [locale]);
101
92
 
102
- // 首页 hero 与浏览器标题跟随外壳标题(回退链在 server 侧走完:def.title → 唯一快照 name →
103
- // 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
104
- // 页头品牌位不归它——那里是恒定的 NiceEval 字标。
93
+ // 浏览器标题是宿主文档单例:跟随外壳标题(回退链在 server 侧走完:def.title →
94
+ // 唯一快照 name → 内置文案「Eval 运行结果 / Eval Results」);缺声明(旧数据)时按内置文案兜底。
95
+ // 页面里的 hero 标题不归宿主——它是页内 Hero 组件,同一取值链经 ctx.report.title 贯通。
105
96
  const shellTitle = localizedText(data.report?.title, locale) ?? t("hero.title");
106
97
  useEffect(() => {
107
98
  document.title = shellTitle;
@@ -123,7 +114,9 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
123
114
  }, []);
124
115
 
125
116
  // 浏览器前进/后退、手改 hash、页内链接(attempt 深链与 `#/page/<id>` 页路由)统一从
126
- // hashchange 分发:attempt hash 开证据室弹窗,页/证据室 hash 切当前 tab。
117
+ // hashchange 分发:attempt hash 开证据室弹窗,页 hash 切当前 tab。
118
+ // attempt 详情路由对有效根解析(viewData.snapshots 即有效根):收窄之内、不在页面统计
119
+ // 口径(现刻水位)里的历史 attempt 仍能经深链打开,报告里的证据引用不因统计口径失效。
127
120
  useEffect(() => {
128
121
  const onHashChange = () => {
129
122
  const locator = parseAttemptHash(location.hash);
@@ -163,9 +156,11 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
163
156
  <Tabs value={tab} onValueChange={(v) => selectTab(v as Tab)}>
164
157
  <header className="topbar">
165
158
  {/* 页头左端是恒定的 NiceEval 品牌字标(与 Powered by 行同族的产品品牌位),
166
- 报告定义不能覆盖或移除;报告 title 的落点是下方 hero 与浏览器标题。 */}
167
- <a className="brand" href={hashForTab(`page:${initialPageId}`)}>
168
- <span className="mark" />
159
+ 报告定义不能覆盖或移除,点击外链官网;报告 title 的落点是页内 hero 与浏览器标题。
160
+ rel noopener 而非 noreferrer:保留 Referer(默认策略只发 origin),官网统计由此
161
+ 得知点击来自哪个报告站点;utm 只负责区分品牌位。 */}
162
+ <a className="brand" href={BRAND_HREF} target="_blank" rel="noopener">
163
+ <span className="mark" aria-hidden="true" />
169
164
  <span>NiceEval</span>
170
165
  </a>
171
166
  <TabsList aria-label={t("nav.label")}>
@@ -174,11 +169,6 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
174
169
  {localizedText(page.title, locale) ?? page.id}
175
170
  </TabsTrigger>
176
171
  ))}
177
- {EVIDENCE_TABS.map((item) => (
178
- <TabsTrigger key={item.id} value={item.id}>
179
- {t(item.label)}
180
- </TabsTrigger>
181
- ))}
182
172
  </TabsList>
183
173
  {data.report?.links?.length ? (
184
174
  <nav className="shell-links" aria-label="Links">
@@ -208,53 +198,14 @@ export function App({ data, reportPages }: { data: ViewData; reportPages: Record
208
198
  </div>
209
199
  </header>
210
200
  <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
201
  {pages.map((page) => (
236
202
  <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
203
  {/* 报告槽:server 侧逐页渲染好的静态 HTML(含 <Style> 产物),按当前页与界面语言
246
- 摆放对应块;Scope 警告由报告页内呈现,壳不设第二条通道。
204
+ 摆放对应块;hero、品牌行、Scope 警告、批量修复 prompt 都是页内组件,壳不再渲染。
247
205
  attempt 深链是普通 <a href="#/attempt/…">,经 hashchange 打开证据室弹窗。 */}
248
206
  <ReportSlot html={reportPages[page.id]?.[locale] || reportPages[page.id]?.en || ""} />
249
207
  </TabsContent>
250
208
  ))}
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
209
  </main>
259
210
  {footerText ? (
260
211
  <footer className="site-footer">