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
package/src/view/data.ts CHANGED
@@ -57,12 +57,6 @@ export interface ViewScan {
57
57
  reportPages: ViewReportPageHtml[];
58
58
  /** 外壳注入资产(styles / scripts;{src} 已按路径纪律解析成 inline 内容),只进 web 面。 */
59
59
  shellAssets: { styles: string[]; scripts: string[]; head: ResolvedHeadTag[] };
60
- /**
61
- * --out 的数据等级(见 docs/feature/reports/view.md「静态导出」):全部选中快照带
62
- * publish:{redaction:"applied"} 才是 "applied",否则 "sensitive"(含本地事实根与
63
- * redaction:"none"——上游声明过原文发布也不豁免导出时的确认)。
64
- */
65
- publishState: "applied" | "sensitive";
66
60
  }
67
61
 
68
62
  /** view 宿主输入的组合语义(与 show 对齐,docs/feature/reports/architecture.md「Scope 是计算入口」)。 */
@@ -273,8 +267,8 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
273
267
  }
274
268
  }
275
269
 
276
- // 全局最新快照(跨全部实验):hero 的「最近一次运行」时刻从这里取。
277
- // hero 标题不从这里取——它走标题回退链(resolveReportTitle,进 viewData.report.title)。
270
+ // 全局最新快照(跨全部实验):viewData.lastRunAt 从这里取。页内 hero 的「最后运行」
271
+ // 显示由 Hero 组件按 heroData(scope) 自己算,不吃这份字段。
278
272
  let latestSnapshot: Snapshot | undefined;
279
273
  for (const exp of results.experiments) {
280
274
  const candidate = exp.snapshots[0];
@@ -292,17 +286,12 @@ export async function loadViewScan(input?: string, opts: ViewScanOptions = {}):
292
286
  skippedRuns: results.skipped.map(toSkippedNotice),
293
287
  report: slot.meta,
294
288
  };
295
- const publishState =
296
- selection.snapshots.length > 0 && selection.snapshots.every((snap) => snap.publish?.redaction === "applied")
297
- ? ("applied" as const)
298
- : ("sensitive" as const);
299
289
  return {
300
290
  viewData,
301
291
  artifactDirs,
302
292
  attemptsByBase,
303
293
  reportPages: slot.pages,
304
294
  shellAssets: slot.shellAssets,
305
- publishState,
306
295
  };
307
296
  }
308
297
 
package/src/view/index.ts CHANGED
@@ -88,23 +88,12 @@ export async function buildView(opts: ViewOptions = {}): Promise<string> {
88
88
  "--out exports the whole results root and cannot be combined with eval prefixes or --experiment.\n" +
89
89
  "To publish a site for one experiment, build a narrower results root and export that:\n" +
90
90
  ' const results = await openResults(".niceeval");\n' +
91
- ' await copySnapshots(results.latest().filter((s) => s.experimentId.startsWith("<prefix>/")), "<publish-root>", { redact });\n' +
91
+ ' await copySnapshots(results.latest().filter((s) => s.experimentId.startsWith("<prefix>/")), "<publish-root>");\n' +
92
92
  "Then: niceeval view --results <publish-root> --out <site>",
93
93
  );
94
94
  }
95
95
  // 静态导出保持「任一页失败整体失败」(pageFailure 缺省 "throw"),不产出半套站点。
96
96
  const plan = await planSite(opts.input, opts.scan);
97
- // --out 按发布防呆二分(见 docs/feature/reports/view.md「静态导出」):目标结果根的全部快照
98
- // 带 publish:{redaction:"applied"}(copySnapshots 补记)时直接导出;redaction:"none"、
99
- // 无标记结果或本地事实根,都必须显式传 --allow-sensitive-artifacts——静态站原样携带证据文件,
100
- // 上游声明过原文发布也不豁免这里的确认。
101
- if (!opts.allowSensitiveArtifacts && plan.scan.publishState !== "applied") {
102
- throw new ViewInputError(
103
- `--out refuses to export unredacted results: not every selected snapshot carries publish: { redaction: "applied" }. ` +
104
- `Produce a publish root first with copySnapshots({ redact }) and export that (niceeval view --results <publish-root> --out <site>), ` +
105
- `or pass --allow-sensitive-artifacts to explicitly export raw evidence (prompts, tool args, full outputs, sources).`,
106
- );
107
- }
108
97
  await writeSite(plan, out);
109
98
  return out;
110
99
  }
@@ -13,8 +13,6 @@ export interface ViewOptions {
13
13
  input?: string;
14
14
  out?: string;
15
15
  port?: number;
16
- /** `--out` 对非发布根(无 publish:applied 标记)导出时的显式确认;静态站原样携带证据文件。 */
17
- allowSensitiveArtifacts?: boolean;
18
16
  /** 报告槽的组合语义(位置前缀 / --experiment / --report),透传给站点管线。 */
19
17
  scan?: ViewScanOptions;
20
18
  }
@@ -33,8 +33,9 @@ export interface ViewReportPageMeta {
33
33
  /**
34
34
  * 规范化后的报告外壳声明(docs/feature/reports/library/shell.md):壳(导航 / 页脚)由前端
35
35
  * 渲染,页内容消费 <template> 静态块。title 已走完回退链(def.title → 唯一且相同的快照 name →
36
- * 内置文案「Eval 运行结果 / Eval Results」),落点是首页 hero 与浏览器标题——页头品牌位恒为
37
- * NiceEval 字标,不归 title;scripts / styles 是注入资产,不进 viewData。
36
+ * 内置文案「Eval 运行结果 / Eval Results」),宿主落点只有浏览器 <title>(文档单例);
37
+ * 页内 hero 标题由 Hero 组件消费同一取值链,品牌是组件、宿主页头不渲染任何品牌位。
38
+ * scripts / styles 是注入资产,不进 viewData。
38
39
  * link 的 icon 是内联 SVG 字符串(原样透传、原样内联),不收组件——viewData 就是序列化边界。
39
40
  */
40
41
  export interface ViewReportMeta {
@@ -70,7 +71,11 @@ export interface ViewSnapshot {
70
71
  results: ViewEvalResult[];
71
72
  }
72
73
 
73
- /** 目录扫描里被跳过的 run 在页面顶部的提示条目;三种原因与 niceeval/results 的 skipped 一致。 */
74
+ /**
75
+ * 目录扫描里被跳过的 run 的结构化条目;三种原因与 niceeval/results 的 skipped 一致。
76
+ * 页面上的呈现不走它:不可读快照已形成 `unreadable-snapshot` Scope warning,由报告页内的
77
+ * `ScopeWarnings` 组件显示;这里只随 viewData 携带原始事实。
78
+ */
74
79
  export interface SkippedRunNotice {
75
80
  /** run 目录,相对 cwd。 */
76
81
  dir: string;
@@ -92,11 +97,11 @@ export interface SkippedRunNotice {
92
97
  export interface ViewData {
93
98
  /** 最近一次 run 的 startedAt(ISO);没有历史 run 时缺省。 */
94
99
  lastRunAt?: string;
95
- /** 报告槽 Selection 合成自几个物理 run;hero「合成来源」标注。 */
100
+ /** 报告槽 Selection 合成自几个物理 run */
96
101
  composedRuns: number;
97
- /** 全部历史快照(跨快照按身份键去重后);修复 prompt latest,Attempts / Traces 吃全部。 */
102
+ /** 全部历史快照(跨快照按身份键去重后);attempt 详情路由对这份完整集合解析,不随报告 Scope 收窄。 */
98
103
  snapshots: ViewSnapshot[];
99
- /** 读不了的落盘(三种原因);前端顶部横幅展示,不静默。 */
104
+ /** 读不了的落盘(三种原因);呈现走报告页内的 ScopeWarnings(unreadable-snapshot warning)。 */
100
105
  skippedRuns?: SkippedRunNotice[];
101
106
  /** 报告外壳与页导航的声明(规范化后);缺省时前端按单页 `report` 兜底。 */
102
107
  report?: ViewReportMeta;
@@ -54,7 +54,7 @@ describe("站点管线奇偶:server 与 --out 是同一份产物", () => {
54
54
  await seedSnapshot(root, "exp", "q1");
55
55
 
56
56
  const out = await makeDir("niceeval-parity-out-");
57
- await buildView({ input: root, out, allowSensitiveArtifacts: true });
57
+ await buildView({ input: root, out });
58
58
  server = await startViewServer({ input: root });
59
59
 
60
60
  // 清单驱动遍历:计划里的每一个路径,写盘字节 ≡ 服务字节。
package/src/view/site.ts CHANGED
@@ -21,7 +21,7 @@ export interface SiteFile {
21
21
  export interface SitePlan {
22
22
  /** path → SiteFile;写盘按它遍历,server 按它查表(查不到即 404,不存在旁路取数)。 */
23
23
  files: Map<string, SiteFile>;
24
- /** 构建这份产物用的扫描结果(publishState 等宿主前置校验用;不进产物)。 */
24
+ /** 构建这份产物用的扫描结果(宿主前置校验与调试用;不进产物)。 */
25
25
  scan: ViewScan;
26
26
  }
27
27
 
@@ -63,8 +63,8 @@ a { color: inherit; }
63
63
  align-items: center;
64
64
  justify-content: space-between;
65
65
  /* space-between 只在有富余空间时才撑开相邻项;.shell-links 的 margin-left:auto 会把富余
66
- 空间全部吸收到它左侧,导致品牌与导航贴脸、shell-links 与语言切换贴脸。显式 gap 保证
67
- 每一对相邻项(品牌—导航、shell-links—语言切换)恒有最小间距,不依赖富余空间。 */
66
+ 空间全部吸收到它左侧,导致相邻项贴脸。显式 gap 保证每一对相邻项(导航—shell-links
67
+ shell-links—语言切换)恒有最小间距,不依赖富余空间。 */
68
68
  gap: 28px;
69
69
  padding: 0 clamp(20px, 5vw, 80px);
70
70
  background: color-mix(in oklch, var(--bg), transparent 4%);
@@ -73,20 +73,7 @@ a { color: inherit; }
73
73
  z-index: 10;
74
74
  backdrop-filter: blur(10px);
75
75
  }
76
- .brand {
77
- display: flex;
78
- align-items: baseline;
79
- gap: 12px;
80
- font-weight: 690;
81
- font-size: 20px;
82
- }
83
- .mark {
84
- width: 18px;
85
- height: 18px;
86
- border: 1.25px solid var(--text);
87
- transform: rotate(45deg);
88
- display: inline-block;
89
- }
76
+ /* 页头无品牌位:品牌是页内组件(PoweredBy / Hero 的品牌行,nre-powered-by),宿主不渲染。 */
90
77
  /* 顶部导航 .nav / .nav-tab → components/ui/tabs.tsx(Radix Tabs + Tailwind utilities)。 */
91
78
  .lang-switch {
92
79
  display: inline-flex;
@@ -121,219 +108,13 @@ main {
121
108
  margin: 0 auto;
122
109
  padding: clamp(52px, 7vw, 82px) 0 72px;
123
110
  }
124
- .hero {
125
- text-align: center;
126
- max-width: 740px;
127
- margin: 0 auto clamp(38px, 6vw, 58px);
128
- }
129
- h1 {
130
- margin: 0;
131
- font-size: clamp(42px, 5.6vw, 66px);
132
- line-height: 0.94;
133
- font-weight: 760;
134
- letter-spacing: 0;
135
- }
136
- .subtitle {
137
- margin: 20px auto 0;
138
- color: color-mix(in oklch, var(--muted), var(--text) 12%);
139
- font-size: clamp(17px, 2vw, 21px);
140
- line-height: 1.58;
141
- max-width: 660px;
142
- }
143
- .meta {
144
- display: flex;
145
- flex-wrap: wrap;
146
- justify-content: center;
147
- gap: 14px;
148
- margin-top: 24px;
149
- color: var(--muted);
150
- font-size: 14px;
151
- }
152
- .meta b { color: var(--text); font-weight: 570; }
153
- /* --report 报告槽:server 侧渲染好的静态报告 HTML(nre-* 类的样式随包 styles.css 注入),
154
- 这里只给槽位留呼吸空间,不覆盖报告自己的样式。 */
111
+ /* 报告槽:server 侧渲染好的静态报告 HTML(nre-* 类的样式随包 styles.css 注入),
112
+ 这里只给槽位留呼吸空间,不覆盖报告自己的样式。hero、警告区、批量修复 prompt 都是
113
+ 页内组件(nre-hero / nre-warnings / nre-copy-fix-prompt),宿主不再有对应 chrome。 */
155
114
  .report-slot {
156
115
  margin: 0 auto 36px;
157
116
  }
158
- /* schemaVersion 不匹配等读不了的 run:报告槽不解析它们,这里占位提示。同一原因(且文案
159
- 完全相同)的条目按 SkippedRunsBanner 分组折叠,不逐条重复整句原因 + 命令——那是原实现
160
- 把首屏挤满、警示色又淡到看不出来的根因。 */
161
- .incompatible-banner {
162
- border: 1px solid color-mix(in oklch, var(--warn), var(--line) 35%);
163
- border-left: 3px solid var(--warn);
164
- border-radius: 8px;
165
- background: color-mix(in oklch, var(--warn), var(--panel) 85%);
166
- padding: 12px 16px 14px;
167
- margin: 0 auto 36px;
168
- font-size: 13px;
169
- }
170
- .ib-head {
171
- display: flex;
172
- align-items: center;
173
- gap: 8px;
174
- }
175
- .ib-head b { font-weight: 640; }
176
- .ib-icon {
177
- width: 16px;
178
- height: 16px;
179
- flex-shrink: 0;
180
- color: var(--warn);
181
- stroke-width: 2;
182
- }
183
- .ib-groups {
184
- display: grid;
185
- gap: 10px;
186
- margin-top: 10px;
187
- }
188
- .ib-group-head {
189
- display: flex;
190
- align-items: baseline;
191
- gap: 8px;
192
- }
193
- .ib-group-count {
194
- display: inline-flex;
195
- align-items: center;
196
- justify-content: center;
197
- min-width: 20px;
198
- height: 20px;
199
- padding: 0 6px;
200
- flex-shrink: 0;
201
- border-radius: 999px;
202
- background: color-mix(in oklch, var(--warn), var(--panel) 72%);
203
- color: var(--text);
204
- font-size: 11px;
205
- font-weight: 640;
206
- font-variant-numeric: tabular-nums;
207
- }
208
- .ib-group-desc { color: var(--soft); }
209
- .ib-group ul {
210
- margin: 5px 0 0;
211
- padding: 0;
212
- list-style: none;
213
- display: grid;
214
- gap: 3px;
215
- }
216
- .ib-group li {
217
- display: flex;
218
- align-items: center;
219
- gap: 8px;
220
- min-height: 22px;
221
- }
222
- .ib-dir {
223
- font-family: var(--mono, ui-monospace, monospace);
224
- font-weight: 560;
225
- }
226
- .ib-more {
227
- color: var(--soft);
228
- font-size: 12px;
229
- }
230
- .ib-copy {
231
- display: inline-flex;
232
- align-items: center;
233
- justify-content: center;
234
- width: 22px;
235
- height: 22px;
236
- flex-shrink: 0;
237
- border: 1px solid var(--line);
238
- border-radius: 5px;
239
- background: var(--panel);
240
- color: var(--muted);
241
- cursor: pointer;
242
- transition: color 0.12s, border-color 0.12s;
243
- }
244
- .ib-copy svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2; }
245
- .ib-copy:hover { color: var(--text); border-color: var(--line-strong); }
246
- .ib-copy.is-copied { color: var(--good); border-color: color-mix(in oklch, var(--good), var(--line) 40%); }
247
- .ib-toggle {
248
- display: inline-flex;
249
- align-items: center;
250
- gap: 4px;
251
- margin-top: 10px;
252
- border: 0;
253
- background: none;
254
- color: var(--muted);
255
- font: inherit;
256
- font-size: 12px;
257
- cursor: pointer;
258
- padding: 0;
259
- }
260
- .ib-toggle svg { width: 12px; height: 12px; transition: transform 0.12s; }
261
- .ib-toggle svg.is-open { transform: rotate(90deg); }
262
- .ib-toggle:hover { color: var(--text); }
263
-
264
- .section-head {
265
- display: flex;
266
- align-items: end;
267
- justify-content: space-between;
268
- gap: 18px;
269
- margin-bottom: 16px;
270
- }
271
- h2 {
272
- margin: 0;
273
- font-size: clamp(24px, 2.6vw, 31px);
274
- line-height: 1.1;
275
- font-weight: 720;
276
- }
277
- .controls {
278
- display: flex;
279
- gap: 8px;
280
- align-items: center;
281
- flex-wrap: wrap;
282
- }
283
- .search {
284
- height: 36px;
285
- min-width: 320px;
286
- border: 1px solid var(--line);
287
- border-radius: 7px;
288
- padding: 0 12px;
289
- background: var(--panel);
290
- color: var(--text);
291
- outline: none;
292
- }
293
- .search:focus { border-color: var(--focus); }
294
- .table-wrap {
295
- border: 1px solid var(--line);
296
- border-radius: 8px;
297
- overflow: auto;
298
- background: var(--panel);
299
- }
300
- table {
301
- width: 100%;
302
- border-collapse: collapse;
303
- min-width: 1040px;
304
- }
305
- th, td {
306
- border-bottom: 1px solid var(--line);
307
- padding: 0 16px;
308
- height: 52px;
309
- text-align: left;
310
- vertical-align: middle;
311
- font-size: 14px;
312
- }
313
- th {
314
- color: var(--muted);
315
- font-weight: 500;
316
- background: var(--panel);
317
- position: sticky;
318
- top: 0;
319
- z-index: 2;
320
- }
321
- tbody tr:last-child td { border-bottom: 0; }
322
- .section-sub-head {
323
- display: flex;
324
- align-items: center;
325
- justify-content: space-between;
326
- gap: 18px;
327
- margin-bottom: 16px;
328
- min-height: 36px;
329
- }
330
- .group-detail-label {
331
- font-size: 15px;
332
- font-weight: 680;
333
- color: var(--text);
334
- }
335
117
  .name { font-weight: 610; }
336
- .sub { color: var(--muted); font-size: 12px; margin-top: 3px; }
337
118
  .num {
338
119
  font-variant-numeric: tabular-nums;
339
120
  white-space: nowrap;
@@ -343,12 +124,6 @@ tbody tr:last-child td { border-bottom: 0; }
343
124
  .bad { color: var(--bad); }
344
125
  .warn { color: var(--warn); }
345
126
  .infra-err { color: var(--warn); }
346
- .eval-id {
347
- display: flex;
348
- align-items: center;
349
- overflow-wrap: anywhere;
350
- font-weight: 560;
351
- }
352
127
  .copy-all-errors {
353
128
  height: 36px;
354
129
  display: inline-flex;
@@ -702,41 +477,10 @@ pre {
702
477
  font-size: 12px;
703
478
  line-height: 1.5;
704
479
  }
705
- .empty {
706
- border: 1px dashed var(--line-strong);
707
- border-radius: 8px;
708
- padding: 40px 24px;
709
- color: var(--muted);
710
- text-align: center;
711
- background: var(--panel);
712
- }
713
- a.brand { text-decoration: none; color: inherit; }
714
- a.brand:hover { opacity: 0.75; }
715
- .traces-entry {
716
- border: 1px solid var(--line);
717
- border-radius: 8px;
718
- background: var(--panel);
719
- margin-bottom: 12px;
720
- padding: 14px 18px;
721
- }
722
- .traces-entry-head {
723
- display: flex;
724
- align-items: baseline;
725
- gap: 12px;
726
- margin-bottom: 8px;
727
- font-size: 13px;
728
- }
729
- .traces-verdict { font-weight: 600; min-width: 52px; }
730
- .traces-exp { color: var(--muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
731
- .traces-dur { color: var(--soft); }
732
480
  @media (max-width: 760px) {
733
481
  .topbar { padding: 0 20px; }
734
482
  .lang-switch button { min-width: 40px; }
735
483
  main { width: min(100vw - 28px, 1120px); padding-top: 42px; }
736
- .hero { text-align: left; margin-left: 0; }
737
- .meta { justify-content: flex-start; }
738
- .section-head { align-items: stretch; flex-direction: column; }
739
- .search { width: 100%; min-width: 0; }
740
484
  .detail { padding: 18px 16px 20px; }
741
485
  .config-strip { gap: 6px; }
742
486
  .detail-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 10px; }
@@ -1194,8 +938,4 @@ li.tnode-row::before { content: ""; width: 4px; flex-shrink: 0; }
1194
938
  color: var(--soft);
1195
939
  font-size: 12px;
1196
940
  }
1197
- /* 品牌行:hero 之下、品牌蓝,恒带官网链接(shell.md「行为约束」)。 */
1198
- .hero .powered-by { display: block; margin-top: 16px; font-size: 12px; }
1199
- .hero .powered-by a { color: var(--blue); text-decoration: none; }
1200
- .hero .powered-by a:hover { text-decoration: underline; }
1201
941
  .nre-page-error pre { white-space: pre-wrap; }
@@ -139,33 +139,55 @@ describe("resolveViewInput · 输入语义", () => {
139
139
  });
140
140
 
141
141
 
142
- /** 单页报告(裸跑 / 树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
142
+ /** 单页报告(树形态 --report)的报告槽 HTML:规范化后唯一页 id 恒为 `report`。 */
143
143
  function slotHtml(scan: ViewScan): { en: string; "zh-CN": string } {
144
144
  expect(scan.reportPages).toHaveLength(1);
145
145
  expect(scan.reportPages[0]!.id).toBe("report");
146
146
  return scan.reportPages[0]!.html as { en: string; "zh-CN": string };
147
147
  }
148
148
 
149
+ /** 多页定义(裸跑装载的内建报告是三页)按页 id 取报告槽 HTML。 */
150
+ function pageHtml(scan: ViewScan, id: string): { en: string; "zh-CN": string } {
151
+ const page = scan.reportPages.find((p) => p.id === id);
152
+ expect(page, `page ${id}`).toBeTruthy();
153
+ return page!.html as { en: string; "zh-CN": string };
154
+ }
155
+
156
+ /** 裸跑的首页(内建报告的 report 页):先断言内建三页齐全,再取首页 HTML。 */
157
+ function bareReportHtml(scan: ViewScan): { en: string; "zh-CN": string } {
158
+ expect(scan.reportPages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
159
+ return pageHtml(scan, "report");
160
+ }
161
+
149
162
  // ───────────────────────── 组合语义(与 show 对齐) ─────────────────────────
150
163
 
151
164
  describe("loadViewScan · 组合语义", () => {
152
- it("位置前缀收窄报告槽 Selection;证据室快照不收窄,深链恒可达", async () => {
165
+ it("位置前缀收窄对全部页生效(含内建 Attempts 页);attempt 详情路由对完整结果根解析,被滤掉的深链仍可达", async () => {
153
166
  const root = await seedRoot();
154
167
  const scan = await loadViewScan(root, { patterns: ["weather"] });
155
168
  const { viewData } = scan;
156
- const reportHtml = slotHtml(scan);
157
- // 报告槽:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
169
+ const reportHtml = bareReportHtml(scan);
170
+ // 报告页:两实验都只剩 weather/brooklyn 一题,范围外的失败不再出现。
158
171
  expect(reportHtml.en).toContain("compare/bub");
159
172
  expect(reportHtml.en).toContain("compare/codex");
160
173
  expect(reportHtml.en).not.toContain("fixtures/button");
161
- // 证据室:快照明细仍含 fixtures/button(attempt 深链在收窄下也能解析)。
162
- const allIds = viewData.snapshots.flatMap((s) => s.results.map((r) => r.id));
163
- expect(allIds).toContain("fixtures/button");
174
+ // Attempts / Traces 是普通页,共享同一收窄后 Scope:行集同样缩小。
175
+ expect(pageHtml(scan, "attempts").en).not.toContain("fixtures/button");
176
+ expect(pageHtml(scan, "traces").en).not.toContain("fixtures/button");
177
+ const bare = await loadViewScan(root);
178
+ expect(pageHtml(bare, "attempts").en).toContain("fixtures/button"); // 不收窄时在场,对照
179
+ // attempt 详情路由不是页:viewData.snapshots 全量通道保留,被滤掉的 attempt 深链仍解析成功。
180
+ const filteredOut = viewData.snapshots
181
+ .flatMap((s) => s.results)
182
+ .find((r) => r.id === "fixtures/button");
183
+ expect(filteredOut?.locator).toBeTruthy();
184
+ const { resolveAttemptLocator } = await import("./app/lib/attempt-route.ts");
185
+ expect(resolveAttemptLocator(viewData.snapshots, filteredOut!.locator!)).toBe(filteredOut);
164
186
  });
165
187
 
166
188
  it("--experiment 过滤:报告槽 Selection 只留该实验", async () => {
167
189
  const root = await seedRoot();
168
- const reportHtml = slotHtml(await loadViewScan(root, { experiment: "compare/codex" }));
190
+ const reportHtml = bareReportHtml(await loadViewScan(root, { experiment: "compare/codex" }));
169
191
  expect(reportHtml.en).toContain("compare/codex");
170
192
  expect(reportHtml.en).not.toContain("compare/bub");
171
193
  });
@@ -177,34 +199,43 @@ describe("loadViewScan · 组合语义", () => {
177
199
  await expect(loadViewScan(root, { experiment: "nosuch" })).rejects.toBeInstanceOf(ViewInputError);
178
200
  });
179
201
 
180
- it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则)", async () => {
202
+ it("全部缺省:报告槽使用现刻水位口径(与 show 相同的合成规则);导航页即内建三页", async () => {
181
203
  const root = await seedRoot();
182
- const reportHtml = slotHtml(await loadViewScan(root));
204
+ const scan = await loadViewScan(root);
205
+ const reportHtml = bareReportHtml(scan);
183
206
  // 默认报告的榜单含两个实验;官方组件的稳定类名在场。
184
207
  expect(reportHtml.en).toContain("compare/bub");
185
208
  expect(reportHtml.en).toContain("compare/codex");
186
209
  expect(reportHtml.en).toContain("nre-");
210
+ // 导航 = 报告定义声明的页(声明序),宿主不追加:裸 view 的三个 tab 就是内建三页。
211
+ expect(scan.viewData.report?.pages.map((p) => p.id)).toEqual(["report", "attempts", "traces"]);
212
+ expect(scan.viewData.report?.pages.map((p) => p.title)).toEqual([
213
+ { en: "Report", "zh-CN": "报告" },
214
+ "Attempts",
215
+ { en: "Traces", "zh-CN": "追踪" },
216
+ ]);
187
217
  });
188
218
  });
189
219
 
190
220
  // ─────────────────── 报告槽恒在:裸跑 ≡ --report <ExperimentComparison> ───────────────────
191
221
 
192
222
  describe("loadViewScan · 默认报告槽(裸跑)", () => {
193
- it("裸跑产出的报告槽 HTML 与 --report <re-export ExperimentComparison 的文件> 完全一致(双语)", async () => {
223
+ it("裸跑产出的报告槽 HTML 与 --report <re-export 内建报告的文件> 逐页完全一致(双语)", async () => {
194
224
  const root = await seedRoot();
195
- const bare = slotHtml(await loadViewScan(root));
196
- const viaReport = slotHtml(
197
- await loadViewScan(root, {
198
- report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
199
- }),
200
- );
201
- expect(bare.en).toBe(viaReport.en);
202
- expect(bare["zh-CN"]).toBe(viaReport["zh-CN"]);
225
+ const bare = await loadViewScan(root);
226
+ const viaReport = await loadViewScan(root, {
227
+ report: { path: DEFAULT_REPORT_REEXPORT, cwd: root },
228
+ });
229
+ expect(viaReport.reportPages.map((p) => p.id)).toEqual(bare.reportPages.map((p) => p.id));
230
+ for (const pageId of ["report", "attempts", "traces"]) {
231
+ expect(pageHtml(viaReport, pageId).en).toBe(pageHtml(bare, pageId).en);
232
+ expect(pageHtml(viaReport, pageId)["zh-CN"]).toBe(pageHtml(bare, pageId)["zh-CN"]);
233
+ }
203
234
  });
204
235
 
205
236
  it("报告槽双语渲染:同一棵树按 locale 渲染两遍,chrome 文案分语言、数据不分语言", async () => {
206
237
  const root = await seedRoot();
207
- const reportHtml = slotHtml(await loadViewScan(root));
238
+ const reportHtml = bareReportHtml(await loadViewScan(root));
208
239
  expect(reportHtml.en).toContain("End-to-end pass rate"); // ExperimentList 主行(en)
209
240
  expect(reportHtml["zh-CN"]).toContain("端到端成功率"); // ExperimentList 主行(zh-CN)
210
241
  for (const html of [reportHtml.en, reportHtml["zh-CN"]]) {
@@ -216,11 +247,17 @@ describe("loadViewScan · 默认报告槽(裸跑)", () => {
216
247
  }
217
248
  });
218
249
 
219
- it("失败清单与警告住在报告槽里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
250
+ it("失败清单与警告住在报告页里:ExperimentList 列出失败,壳的 viewData 不携带统计产物", async () => {
220
251
  const root = await seedRoot();
221
252
  const scan = await loadViewScan(root);
222
253
  const { viewData } = scan;
223
- expect(slotHtml(scan).en).toContain("fixtures/button");
254
+ expect(bareReportHtml(scan).en).toContain("fixtures/button");
255
+ // 品牌行是页内组件(Hero 恒含),不是宿主 chrome:每页 web 面都带官网品牌行。
256
+ for (const pageId of ["report", "attempts", "traces"]) {
257
+ const html = pageHtml(scan, pageId).en;
258
+ expect(html).toContain("nre-powered-by");
259
+ expect(html).toContain("https://niceeval.com/?utm_source=report&amp;utm_medium=powered-by");
260
+ }
224
261
  // viewData 只有证据室数据:快照 + skipped + 壳元信息。
225
262
  expect(viewData).not.toHaveProperty("overview");
226
263
  expect(viewData).not.toHaveProperty("table");
@@ -398,7 +435,7 @@ describe("buildView · --out 与 --report", () => {
398
435
  await writeFile(join(artifactDir, "events.json"), "[]", "utf-8");
399
436
 
400
437
  const out = join(root, "site");
401
- await buildView({ input: root, out, allowSensitiveArtifacts: true, scan: { report: { path: EXAM_REPORT, cwd: root } } });
438
+ await buildView({ input: root, out, scan: { report: { path: EXAM_REPORT, cwd: root } } });
402
439
 
403
440
  const html = await readFile(join(out, "index.html"), "utf-8");
404
441
  // 双语两个 <template> 静态块都在,壳按界面语言摆放。
@@ -424,7 +461,7 @@ describe("buildView · --out 与 --report", () => {
424
461
  await writeFile(join(artifactDir, "o11y.json"), "{}", "utf-8");
425
462
 
426
463
  const out = join(root, "site");
427
- await buildView({ input: root, out, allowSensitiveArtifacts: true });
464
+ await buildView({ input: root, out });
428
465
  const exported = join(out, "artifact", "compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a0");
429
466
  expect(existsSync(join(exported, "diff.json"))).toBe(true);
430
467
  expect(existsSync(join(exported, "events.json"))).toBe(true);
@@ -435,10 +472,10 @@ describe("buildView · --out 与 --report", () => {
435
472
  const root = await seedRoot();
436
473
  const out = join(root, "site");
437
474
  for (const scan of [{ patterns: ["weather"] }, { experiment: "compare/bub" }]) {
438
- const attempt = buildView({ input: root, out, allowSensitiveArtifacts: true, scan });
475
+ const attempt = buildView({ input: root, out, scan });
439
476
  await expect(attempt).rejects.toBeInstanceOf(ViewInputError);
440
477
  await expect(
441
- buildView({ input: root, out, allowSensitiveArtifacts: true, scan }),
478
+ buildView({ input: root, out, scan }),
442
479
  ).rejects.toThrow(/copySnapshots/);
443
480
  }
444
481
  // 同参数不带 --out 时照常收窄报告槽(不报错)。
@@ -448,7 +485,7 @@ describe("buildView · --out 与 --report", () => {
448
485
  it("默认导出(无 --report):报告槽填充 ExperimentComparison,双语块与增强 runtime 恒内联", async () => {
449
486
  const root = await seedRoot();
450
487
  const out = join(root, "site");
451
- await buildView({ input: root, out, allowSensitiveArtifacts: true });
488
+ await buildView({ input: root, out });
452
489
  const html = await readFile(join(out, "index.html"), "utf-8");
453
490
  expect(html).toContain('<template id="niceeval-report-report-en">');
454
491
  expect(html).toContain('<template id="niceeval-report-report-zh-CN">');