niceeval 0.6.2 → 0.7.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 (218) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/lib/artifact-url.ts +14 -3
  179. package/src/view/app/main.tsx +13 -8
  180. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  181. package/src/view/app/types.ts +4 -1
  182. package/src/view/artifact-serving.test.ts +21 -1
  183. package/src/view/client-dist/app.css +1 -1
  184. package/src/view/client-dist/app.js +6 -6
  185. package/src/view/data.test.ts +9 -3
  186. package/src/view/data.ts +145 -49
  187. package/src/view/index.ts +48 -44
  188. package/src/view/server.ts +35 -15
  189. package/src/view/shared/types.ts +34 -5
  190. package/src/view/styles.css +224 -0
  191. package/src/view/view-report.test.ts +161 -57
  192. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  193. package/dist/report/built-ins/experiment-comparison.js +0 -119
  194. package/dist/report/built-ins/index.d.ts +0 -2
  195. package/dist/report/built-ins/index.js +0 -2
  196. package/dist/report/react/GroupSummary.d.ts +0 -8
  197. package/dist/report/react/GroupSummary.js +0 -8
  198. package/dist/report/react/RunOverview.d.ts +0 -8
  199. package/dist/report/react/RunOverview.js +0 -12
  200. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  201. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  202. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  203. package/docs-site/zh/example/showcase.mdx +0 -39
  204. package/src/report/built-in-user-parity.test.tsx +0 -597
  205. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  206. package/src/report/built-ins/index.ts +0 -7
  207. package/src/report/react/GroupSummary.tsx +0 -66
  208. package/src/report/react/RunOverview.tsx +0 -109
  209. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  213. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  214. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  217. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  218. /package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +0 -0
package/src/show/index.ts CHANGED
@@ -1,17 +1,18 @@
1
- // niceeval show —— 终端宿主(行为规范:docs-site/zh/guides/viewing-results.mdx;
2
- // 宿主组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。
1
+ // niceeval show —— 终端宿主(行为规范:docs/feature/reports/show.md 与分篇;
2
+ // 宿主组合语义:docs/feature/reports/architecture.md「Scope 是计算入口」)。
3
3
  //
4
4
  // 位置参数 = eval id 前缀,或 `@<locator>`(精确指名单个 attempt,见 results/locator.ts):
5
- // 裸跑 / 多 eval 前缀 默认 ExperimentComparison 的 text 面(单 eval 前缀仍进入详情)
5
+ // 裸跑 / 多 eval 前缀 内建报告(niceeval/report/built-in 默认导出)的 text 面(单 eval 前缀仍进入详情)
6
6
  // 恰好一个 eval 单 eval 详情(attempt / 断言明细,宿主本体)
7
7
  // @<locator> 精确 attempt:无证据 flag → 紧凑全景;带 flag → 对应证据切面
8
- // --eval / --execution / --diff[=路径] 证据切面(宿主本体):出现即走证据室,不渲染报告槽
9
- // --history run 时间轴(内置趋势视图),与 --report 互斥
10
- // --report <文件> 整槽换成用户报告;位置前缀 / --run / --experiment 先收窄 Selection 再注入
11
- // --run <目录> 结果根换成该目录;--experiment Selection 只留该实验
8
+ // --source / --execution / --diff[=路径] 证据切面(宿主本体):出现即走证据室,不渲染报告槽
9
+ // --history 执行时间轴(逐 experimentId + evalId 分节),与 --report 互斥
10
+ // --report <文件> 整槽换成用户报告;位置前缀 / --results / --experiment 先收窄 Scope 再注入
11
+ // --page <id> 多页报告选页;未命中列出可用页 id 按用法错误退出
12
+ // --results <目录> 结果根换成该目录;--experiment 让 Scope 只留该实验
12
13
  //
13
- // 数据全部走 niceeval/results 的读取面(openResults + 合成 Selection + loadAttemptEvidence),
14
- // 不自己爬目录;证据可用性只由 loadAttemptEvidence 在单 Attempt 页面计算。
14
+ // 数据全部走 niceeval/results 的读取面(openResults + 合成 Scope + loadAttemptEvidence),
15
+ // 不自己爬目录;证据可用性只由 loadAttemptEvidence 在单 Attempt 页面计算。
15
16
 
16
17
  import { existsSync } from "node:fs";
17
18
  import { join, resolve } from "node:path";
@@ -23,33 +24,38 @@ import {
23
24
  LocatorNotFoundError,
24
25
  MalformedLocatorError,
25
26
  } from "../results/index.ts";
26
- // report.ts / load.ts have no JSX of their own, but ReportDefinition / ReportLoadError must
27
- // come from the SAME module instance ExperimentComparison (built-ins, .tsx) is built
28
- // against `unique symbol` branding and `instanceof` are keyed by declaration site, so a raw
29
- // src copy and the compiled dist copy of "the same" ReportDefinition are, to TypeScript and to
30
- // `instanceof`, two different types. The package-owned report runtime ships as precompiled
31
- // ESM (dist/report/**, built by `pnpm run build:report`, immune to a consumer's cwd/tsconfig
32
- // when niceeval is linked in — see tsconfig.report-build.json); show pulls all of it from
33
- // there, not just the .tsx-touching pieces.
34
- import { renderReportToText } from "../../dist/report/report.js";
35
- import { ExperimentComparison } from "../../dist/report/built-ins/index.js";
36
- import { ReportLoadError, loadReportFile } from "../../dist/report/load.js";
27
+ // ReportLoadError must come from the SAME module instance the report runtime is built
28
+ // against `instanceof` is keyed by declaration site, so a raw src copy and the compiled
29
+ // dist copy of "the same" class are two different types. The package-owned report runtime
30
+ // ships as precompiled ESM (dist/report/**, built by `pnpm run build:report`); all report
31
+ // loading/rendering goes through ./report-host.ts (the single contact surface with it).
32
+ import { ReportLoadError } from "../../dist/report/load.js";
37
33
  import { detectLocale, t } from "../i18n/index.ts";
38
34
  import { foldEvalVerdict } from "../shared/verdict.ts";
39
35
  import { selectCurrentResults, filterExperiments } from "../results/select.ts";
40
- import { evalHistory, experimentHistory } from "./compose.ts";
36
+ import { evalPrefixPredicate } from "../shared/aggregate.ts";
37
+ import { attemptHistory } from "./compose.ts";
38
+ import {
39
+ HostReportError,
40
+ loadHostReport,
41
+ localizeText,
42
+ reportMetaFor,
43
+ renderHostPageText,
44
+ resolveReportTitle,
45
+ type HostCommandContext,
46
+ } from "./report-host.ts";
41
47
  import {
42
48
  attemptArtifactsPath,
43
49
  attemptEvidenceHeader,
50
+ attemptHistoryText,
44
51
  attemptIndexLine,
45
52
  attemptOverviewText,
46
53
  attemptsOfEval,
47
54
  diffText,
48
55
  evalDetailText,
49
- evalHistoryText,
50
56
  evalSourceText,
51
57
  executionText,
52
- experimentHistoryText,
58
+ pageIndexText,
53
59
  timingText,
54
60
  pickDetailAttempt,
55
61
  skippedRunsText,
@@ -57,8 +63,8 @@ import {
57
63
  } from "./render.ts";
58
64
 
59
65
  export interface ShowFlags {
60
- /** attempt 运行时保存的 Eval 源码,断言标回源码行(证据切面)。 */
61
- eval?: boolean;
66
+ /** --source:该 attempt 运行时保存的 Eval 源码,断言标回源码行(证据切面)。 */
67
+ source?: boolean;
62
68
  /** 该 attempt 的标准执行事件流 + OTel enrichment(证据切面)。 */
63
69
  execution?: boolean;
64
70
  /** --timing:默认有界诊断投影;full 逐节点展开。boolean 仅供库调用兼容,等价 summary。 */
@@ -69,8 +75,11 @@ export interface ShowFlags {
69
75
  diffPath?: string;
70
76
  history?: boolean;
71
77
  experiment?: string;
72
- run?: string;
78
+ /** --results:结果根目录(某次快照根或 `copySnapshots` 产物)。 */
79
+ results?: string;
73
80
  report?: string;
81
+ /** --page:多页报告选页;未命中按用法错误退出并列出可用页 id。 */
82
+ page?: string;
74
83
  }
75
84
 
76
85
  /** 注入 IO 供测试;默认写 stdout/stderr、宽度取终端列数。 */
@@ -89,8 +98,8 @@ function clampWidth(columns: number | undefined): number {
89
98
  return Math.max(40, Math.min(columns as number, 160));
90
99
  }
91
100
 
92
- // --report 的装载移到中性模块(两个宿主共用),show 的导出面与错误行为不变。
93
- export { loadReportFile } from "../../dist/report/load.js";
101
+ // --report 的装载与规范化住在 ./report-host.ts(两个宿主共用的唯一联系面)
102
+ export { loadHostReport, normalizeHostReport, resolveReportTitle, localizeText } from "./report-host.ts";
94
103
 
95
104
  export async function runShow(
96
105
  cwd: string,
@@ -109,7 +118,7 @@ export async function runShow(
109
118
  });
110
119
  return 0;
111
120
  } catch (e) {
112
- if (e instanceof ShowError || e instanceof ReportLoadError) {
121
+ if (e instanceof ShowError || e instanceof ReportLoadError || e instanceof HostReportError) {
113
122
  err(e.message.endsWith("\n") ? e.message : `${e.message}\n`);
114
123
  return 1;
115
124
  }
@@ -124,7 +133,7 @@ async function show(
124
133
  io: { out: (s: string) => void; err: (s: string) => void; width: number; now: number },
125
134
  ): Promise<void> {
126
135
  const evidence =
127
- flags.eval === true ||
136
+ flags.source === true ||
128
137
  flags.execution === true ||
129
138
  (flags.timing !== undefined && flags.timing !== false) ||
130
139
  flags.diff === true ||
@@ -135,8 +144,15 @@ async function show(
135
144
  throw new ShowError(t("cli.show.historyReportConflict"));
136
145
  }
137
146
 
138
- const root = flags.run !== undefined ? resolve(cwd, flags.run) : join(cwd, ".niceeval");
139
- if (flags.run !== undefined && !existsSync(root)) {
147
+ // --page 只在报告槽里有意义:证据切面 / 时间轴与它组合是用法矛盾,先于任何 IO 报出来。
148
+ if (flags.page !== undefined && (evidence || flags.history)) {
149
+ throw new ShowError(
150
+ `--page selects a report page and cannot be combined with ${flags.history ? "--history" : "evidence flags"}.\n`,
151
+ );
152
+ }
153
+
154
+ const root = flags.results !== undefined ? resolve(cwd, flags.results) : join(cwd, ".niceeval");
155
+ if (flags.results !== undefined && !existsSync(root)) {
140
156
  throw new ShowError(t("cli.show.runDirMissing", { dir: root }));
141
157
  }
142
158
 
@@ -150,7 +166,7 @@ async function show(
150
166
  // (`@` 打头对 eval id 天然无歧义,见 locator.ts),必须在下面的前缀匹配逻辑之前分流掉,
151
167
  // 不然 "@1x7f3q" 会被当成一个谁都匹配不到的 eval id 前缀,报「no eval match」这种文不对题的
152
168
  // 错误。这一步只解析并渲染出「当前 show 对单个已解析 attempt 能渲染的东西」(单 eval 详情 /
153
- // 三个证据切面)——真正的 `--eval`/`--execution`/`--diff` 统一 attempt 全景是后续阶段。
169
+ // 三个证据切面)——真正的 `--source`/`--execution`/`--diff` 统一 attempt 全景是后续阶段。
154
170
  const locatorArg = patterns.find((p) => p.startsWith(ATTEMPT_LOCATOR_PREFIX));
155
171
  if (locatorArg !== undefined) {
156
172
  if (patterns.length !== 1) {
@@ -171,7 +187,7 @@ async function show(
171
187
  const artifactPath = attemptArtifactsPath(attempt, cwd);
172
188
  if (evidence) {
173
189
  const blocks: string[] = [];
174
- if (flags.eval) blocks.push(evalSourceText(attemptEvidence, { header, artifactPath, width: io.width }));
190
+ if (flags.source) blocks.push(evalSourceText(attemptEvidence, { header, artifactPath, width: io.width }));
175
191
  if (flags.execution) blocks.push(executionText(attemptEvidence, { header, artifactPath, width: io.width }));
176
192
  if (flags.timing) blocks.push(timingText(attemptEvidence, {
177
193
  header,
@@ -240,7 +256,7 @@ async function show(
240
256
  const header = attemptEvidenceHeader(attemptEvidence);
241
257
  const artifactPath = attemptArtifactsPath(picked, cwd);
242
258
  const blocks: string[] = [];
243
- if (flags.eval) blocks.push(evalSourceText(attemptEvidence, { header, artifactPath, width: io.width }));
259
+ if (flags.source) blocks.push(evalSourceText(attemptEvidence, { header, artifactPath, width: io.width }));
244
260
  if (flags.execution) blocks.push(executionText(attemptEvidence, { header, artifactPath, width: io.width }));
245
261
  if (flags.timing) blocks.push(timingText(attemptEvidence, {
246
262
  header,
@@ -255,30 +271,30 @@ async function show(
255
271
  return;
256
272
  }
257
273
 
258
- // --history:内置趋势视图。时间轴只列真实执行 —— resume 携带的复印件不占行。
274
+ // --history:执行时间轴(docs/feature/reports/show.md「--history:一个 eval 的执行时间轴」)。
275
+ // 对 Scope 中匹配的每个 experimentId + evalId 分节,逐 attempt 而非逐快照;时间轴只列
276
+ // 真实执行 —— resume 携带的复印件按 attempt 身份键去重后不占行。
259
277
  if (flags.history) {
260
278
  const experiments = filterExperiments(results.experiments, flags.experiment);
279
+ // eval 位置参数与 Scope 选择用同一个前缀谓词(单点在 shared/aggregate.ts),不另立口径。
280
+ const matchesPattern = patterns.length > 0 ? evalPrefixPredicate(patterns) : () => true;
261
281
  const blocks: string[] = [];
262
- if (patterns.length === 0) {
263
- for (const exp of experiments) blocks.push(experimentHistoryText(exp.id, experimentHistory(exp)));
264
- } else {
265
- const multi = matchedEvalIds.length > 1;
266
- for (const evalId of matchedEvalIds) {
267
- for (const exp of experiments) {
268
- const rows = evalHistory(exp, evalId);
269
- if (rows.length === 0) continue;
270
- blocks.push(evalHistoryText({ experimentId: exp.id, ...(multi ? { evalId } : {}), rows }));
271
- }
282
+ for (const exp of experiments) {
283
+ const evalIds = [...exp.evalIds].filter(matchesPattern).sort();
284
+ for (const evalId of evalIds) {
285
+ const rows = attemptHistory(exp, evalId);
286
+ if (rows.length === 0) continue;
287
+ blocks.push(attemptHistoryText({ experimentId: exp.id, evalId, rows }));
272
288
  }
273
289
  }
274
290
  io.out(blocks.join("\n\n") + "\n");
275
291
  return;
276
292
  }
277
293
 
278
- // 单 eval 详情(宿主本体);--report 在场时报告槽优先,前缀只用来收窄 Selection
294
+ // 单 eval 详情(宿主本体);--report / --page 在场时报告槽优先,前缀只用来收窄 Scope
279
295
  // 挑哪个 attempt 展开明细不再收数字 --attempt——pickDetailAttempt 的默认启发式
280
296
  // (最新一次失败,没有失败挑最新一次)是唯一路径;精确选某一次走 `@<locator>`。
281
- if (flags.report === undefined && patterns.length > 0 && matchedEvalIds.length === 1) {
297
+ if (flags.report === undefined && flags.page === undefined && patterns.length > 0 && matchedEvalIds.length === 1) {
282
298
  const evalId = matchedEvalIds[0];
283
299
  const attempts = attemptsOfEval(selection.snapshots, evalId);
284
300
  const detail = pickDetailAttempt(attempts);
@@ -295,13 +311,48 @@ async function show(
295
311
  return;
296
312
  }
297
313
 
298
- // show 与裸 view 选择同一个普通默认 definition,这里只是渲染 text 面。
299
- // --report 替换同一个报告槽。locale = CLI 界面语言(NICEEVAL_LANG / LC_* / LANG 检测):报告 chrome 文案跟随
300
- // 终端语言(docs/feature/reports/library.md「locale:渲染面的语言」);Locale ReportLocale 同为
301
- // "en" | "zh-CN",直接传递。
302
- const definition = flags.report === undefined ? ExperimentComparison : await loadReportFile(cwd, flags.report);
303
- // attemptCommand 留给 renderReportToText 的默认值:AttemptLocator 已经是可直接 `niceeval show
304
- // @<locator>` 的真实 CLI 语法,不需要再反查 eval id 拼一条近似命令(见 tree.ts 的默认实现)。
305
- const text = await renderReportToText(definition, { selection, results }, { width: io.width, locale: detectLocale() });
314
+ // 报告槽:裸 show 装载 `niceeval/report/built-in` 的默认导出,--report 整槽替换——同一条
315
+ // 「装载 规范化(外壳 + 非空页列表)→ 逐页渲染」管线(docs/feature/reports/library/shell.md)
316
+ // locale = CLI 界面语言(NICEEVAL_LANG / LC_* / LANG 检测):报告 chrome 文案跟随终端语言。
317
+ const report = await loadHostReport(cwd, flags.report);
318
+ const locale = detectLocale();
319
+ const commandContext: HostCommandContext = {
320
+ patterns,
321
+ ...(flags.results !== undefined ? { results: flags.results } : {}),
322
+ ...(flags.report !== undefined ? { report: flags.report } : {}),
323
+ ...(flags.experiment !== undefined ? { experiment: flags.experiment } : {}),
324
+ };
325
+ const sourceLabel = flags.report ?? "the built-in report";
326
+
327
+ let page = report.pages[0];
328
+ if (flags.page !== undefined) {
329
+ const hit = report.pages.find((p) => p.id === flags.page);
330
+ if (!hit) {
331
+ // 用法错误:列出可用页 id(docs/feature/reports/show/reports.md Case 1/2 的报错样例)。
332
+ throw new ShowError(
333
+ `error: page "${flags.page}" not found in ${sourceLabel}. Available pages: ${report.pages.map((p) => p.id).join(", ")}\n`,
334
+ );
335
+ }
336
+ page = hit;
337
+ } else if (report.pages.length > 1) {
338
+ // 多页未选页:只输出页索引与可复制的单页命令,不倾倒页内容(与可比组索引同一模式)。
339
+ // 标题行走标题回退链(终点是内置文案「Eval 运行结果 / Eval Results」,恒有值)。
340
+ const title = localizeText(resolveReportTitle(report.title, selection.snapshots), locale) ?? "Eval Results";
341
+ io.out(pageIndexText({ report, title, command: commandContext, locale }) + "\n");
342
+ return;
343
+ }
344
+
345
+ // attemptCommand 留给渲染管线的默认值:AttemptLocator 已经是可直接 `niceeval show @<locator>`
346
+ // 的真实 CLI 语法,不需要再反查 eval id 拼一条近似命令。
347
+ const meta = reportMetaFor(report, page.id, selection.snapshots);
348
+ const text = await renderHostPageText(
349
+ page,
350
+ { scope: selection, results, report: meta },
351
+ {
352
+ width: io.width,
353
+ locale,
354
+ commandContext: { ...commandContext, ...(flags.page !== undefined ? { page: flags.page } : {}) },
355
+ },
356
+ );
306
357
  io.out(text + "\n");
307
358
  }
@@ -1,5 +1,5 @@
1
1
  // show 的文本渲染:单 eval 详情、--history 时间轴、三个证据切面(transcript / trace / diff)。
2
- // 输出形态照 docs-site/zh/guides/viewing-results.mdx 的示例块;长内容一律截断,
2
+ // 输出形态照 docs-site/zh/how-to/viewing-results.mdx 的示例块;长内容一律截断,
3
3
  // 但截断永远如实标注剩余数量和原始 artifact 路径 —— 输出对上下文窗口友好,事实源留在盘上。
4
4
  // 全部纯函数(时间经 now 显式传入),证据数据由调用方 await 好了递进来。
5
5
 
@@ -15,7 +15,8 @@ import { summaryText } from "../scoring/display.ts";
15
15
  import { attemptCostUSD } from "../report/metrics.ts";
16
16
  import { formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD } from "../report/format.ts";
17
17
  import { indentBlock, padDisplay, renderAlignedRows, wrapDisplay } from "../report/text/layout.ts";
18
- import type { EvalHistoryRow, ExperimentHistoryRow } from "./compose.ts";
18
+ import type { AttemptHistoryRow } from "./compose.ts";
19
+ import { localizeText, showCommand, type HostCommandContext, type HostReport } from "./report-host.ts";
19
20
 
20
21
  const MISSING = "—";
21
22
 
@@ -72,8 +73,8 @@ export function attemptArtifactsPath(attempt: AttemptHandle, cwd: string): strin
72
73
 
73
74
  /**
74
75
  * 裸 `show` 零可读结果时,skipped 目录的展示文案。niceeval 自己写的、schemaVersion 不兼容的
75
- * 落盘按 producer 版本分组,一条 `npx niceeval@<version> show --run <结果根>` 覆盖同版本全部
76
- * 快照——show 没有 view 的单快照直读模式,`--run` 认的是结果根(其下可以有多个 experiment),
76
+ * 落盘按 producer 版本分组,一条 `npx niceeval@<version> show --results <结果根>` 覆盖同版本全部
77
+ * 快照——show 没有 view 的单快照直读模式,`--results` 认的是结果根(其下可以有多个 experiment),
77
78
  * 不是单个快照目录,所以不对每个目录重复拼一条各自的命令(那条命令用同一个 root 跑起来
78
79
  * 结果完全一样,重复只会刷屏)。其余(第三方 harness、版本信息缺失、malformed、incomplete)
79
80
  * 没有可执行的统一建议,原样逐条列出。
@@ -86,7 +87,7 @@ export function skippedRunsText(skipped: readonly SkippedDir[], root: string, cw
86
87
  const count = g.dirs.length;
87
88
  const version = g.producer.version;
88
89
  const schema = g.schemaVersion !== undefined ? ` (schemaVersion ${g.schemaVersion})` : "";
89
- const cmd = `npx niceeval@${version} show --run ${rootDisplay}`;
90
+ const cmd = `npx niceeval@${version} show --results ${rootDisplay}`;
90
91
  lines.push(
91
92
  ` ${count} snapshot${count === 1 ? "" : "s"} written by niceeval ${version}${schema} — run \`${cmd}\` to view`,
92
93
  );
@@ -374,8 +375,8 @@ export function evalDetailText(opts: EvalDetailOptions): string {
374
375
  if (detail.locator) tail.push(`attempt locator: ${detail.locator}`);
375
376
  tail.push(
376
377
  detail.locator
377
- ? `next: niceeval show ${detail.locator} [--eval|--execution|--diff]`
378
- : `next: niceeval show ${evalId} [--eval|--execution|--diff]`,
378
+ ? `next: niceeval show ${detail.locator} [--source|--execution|--diff]`
379
+ : `next: niceeval show ${evalId} [--source|--execution|--diff]`,
379
380
  );
380
381
  blocks.push(tail.join("\n"));
381
382
  }
@@ -385,43 +386,58 @@ export function evalDetailText(opts: EvalDetailOptions): string {
385
386
 
386
387
  // ───────────────────────── --history ─────────────────────────
387
388
 
388
- /** 单 eval 时间轴:`compare/codex-gpt-5.4 · 5 runs · passed 2/5` + 每次真实执行一行。 */
389
- export function evalHistoryText(opts: {
389
+ /**
390
+ * 一个 experimentId + evalId 的执行时间轴分节(docs/feature/reports/show.md「--history」):
391
+ * 节头 `<evalId> · <experimentId> · N attempts · passed x/N`,节内每行
392
+ * 时间 / verdict / 单行摘要 / 耗时 / 成本 / locator,startedAt 升序(compose 已排好)。
393
+ */
394
+ export function attemptHistoryText(opts: {
390
395
  experimentId: string;
391
- /** 多 eval 前缀时块头带上 eval id;单 eval 与 mdx 示例一致不带。 */
392
- evalId?: string;
393
- rows: EvalHistoryRow[];
396
+ evalId: string;
397
+ rows: AttemptHistoryRow[];
394
398
  }): string {
395
399
  const { experimentId, evalId, rows } = opts;
396
400
  const passed = rows.filter((r) => r.verdict === "passed").length;
397
401
  const head = [
398
- ...(evalId ? [evalId] : []),
402
+ evalId,
399
403
  experimentId,
400
- `${rows.length} ${rows.length === 1 ? "run" : "runs"}`,
404
+ attemptsLabel(rows.length),
401
405
  `passed ${passed}/${rows.length}`,
402
406
  ].join(" · ");
403
407
  if (rows.length === 0) return head;
404
408
  const table = renderAlignedRows(
405
409
  rows.map((r) => [
406
- timelineStamp(r.startedAt),
410
+ r.startedAt !== undefined ? timelineStamp(r.startedAt) : MISSING,
407
411
  `${verdictMark(r.verdict)} ${r.verdict}`,
408
- attemptsLabel(r.attempts),
412
+ r.summary ?? MISSING,
413
+ formatDurationMs(r.durationMs),
409
414
  r.costUSD === null ? MISSING : formatUSD(r.costUSD),
410
- r.failedAssertion ?? (r.error ? `error: ${r.error}` : ""),
415
+ r.locator ?? MISSING,
411
416
  ]),
412
417
  );
413
418
  return `${head}\n\n${indentBlock(table, " ")}`;
414
419
  }
415
420
 
416
- /** 实验级 per-run 通过率序列(裸 `show --history`)。 */
417
- export function experimentHistoryText(experimentId: string, rows: ExperimentHistoryRow[]): string {
418
- const head = `${experimentId} · ${rows.length} ${rows.length === 1 ? "run" : "runs"}`;
419
- if (rows.length === 0) return head;
421
+ // ───────────────────────── --report 页索引 ─────────────────────────
422
+
423
+ /**
424
+ * 多页报告的页索引(docs/feature/reports/show/reports.md Case 2):标题行 + 每页一行
425
+ * (id / 本 locale 页名 / 可复制的 `--page` 命令)。索引命令携带完整上下文
426
+ * (--results / --report / 位置参数),复制即可精确复现下一层视图。
427
+ */
428
+ export function pageIndexText(opts: {
429
+ report: HostReport;
430
+ title: string;
431
+ command: HostCommandContext;
432
+ locale: string;
433
+ }): string {
434
+ const { report, title, command, locale } = opts;
435
+ const head = `${title} · ${locale === "zh-CN" ? `${report.pages.length} 页` : `${report.pages.length} pages`}`;
420
436
  const table = renderAlignedRows(
421
- rows.map((r) => [
422
- timelineStamp(r.startedAt),
423
- `${r.passedEvals}/${r.totalEvals} passed`,
424
- r.costUSD === null ? MISSING : formatUSD(r.costUSD),
437
+ report.pages.map((page) => [
438
+ page.id,
439
+ localizeText(page.title, locale) ?? page.id,
440
+ showCommand({ ...command, page: page.id }),
425
441
  ]),
426
442
  );
427
443
  return `${head}\n\n${indentBlock(table, " ")}`;
@@ -603,7 +619,7 @@ function execBody(
603
619
  /**
604
620
  * action / subagent 节点渲染成两行(call + result)——节点模型把 call+result 合并成一个
605
621
  * ExecutionNode(按 callId),但分两行读更符合「先看调用、再看结果」的阅读顺序,
606
- * 与 docs-site/zh/guides/agent-feedback-loop.mdx 的示例一致。
622
+ * 与 docs-site/zh/how-to/agent-feedback-loop.mdx 的示例一致。
607
623
  */
608
624
  function executionNodeLines(node: ExecutionNode, originMs: number, timingAvailable: boolean, width: number): string[] {
609
625
  const time = node.kind !== "telemetry" && node.span ? relSeconds(node.span.startMs, originMs) : undefined;
@@ -889,7 +905,7 @@ export function attemptOverviewText(
889
905
  const tail: string[] = [];
890
906
  if (artifactPath) tail.push(`artifacts: ${artifactPath}/`);
891
907
  const available = [
892
- evidence.capabilities.eval ? `niceeval show ${evidence.locator} --eval` : undefined,
908
+ evidence.capabilities.source ? `niceeval show ${evidence.locator} --source` : undefined,
893
909
  evidence.capabilities.execution ? `niceeval show ${evidence.locator} --execution` : undefined,
894
910
  evidence.capabilities.timing ? `niceeval show ${evidence.locator} --timing` : undefined,
895
911
  evidence.capabilities.diff ? `niceeval show ${evidence.locator} --diff` : undefined,
@@ -0,0 +1,188 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
2
+ // 「外壳、页面与 Tabs」——宿主装载规范化、页索引命令上下文与标题回退链
3
+ // (契约:docs/feature/reports/library/shell.md)。
4
+ // 页内树的 resolve / render 归报告库测试;这里只测宿主侧的规范化与选择逻辑(纯函数)。
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import {
8
+ BUILT_IN_PAGE_TITLE,
9
+ BUILT_IN_REPORT_TITLE,
10
+ HostReportError,
11
+ localizeText,
12
+ localizedTextEquals,
13
+ normalizeHostReport,
14
+ resolveReportTitle,
15
+ showCommand,
16
+ } from "./report-host.ts";
17
+ import { pageIndexText } from "./render.ts";
18
+
19
+ const tree = { kind: "node" }; // 页 content 对宿主是不透明值,规范化不解析树
20
+
21
+ describe("装载规范化:外壳 + 非空页列表", () => {
22
+ it("content 缩写展开为唯一页 id `report`,页名是内置页名「报告 / Report」", () => {
23
+ const report = normalizeHostReport({ kind: "report", content: tree }, "reports/frontier.tsx");
24
+ expect(report.pages).toHaveLength(1);
25
+ expect(report.pages[0]).toMatchObject({ id: "report", title: BUILT_IN_PAGE_TITLE, content: tree });
26
+ expect(report.links).toEqual([]);
27
+ });
28
+
29
+ it("pages 形态按声明序保留页列表与外壳字段", () => {
30
+ const report = normalizeHostReport(
31
+ {
32
+ kind: "report",
33
+ title: { en: "Memory Evals", "zh-CN": "记忆能力评测" },
34
+ links: [{ label: "GitHub", href: "https://example.com" }],
35
+ footer: "Published nightly.",
36
+ pages: [
37
+ { id: "overview", title: { en: "Overview", "zh-CN": "总览" }, content: tree },
38
+ { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" }, content: tree },
39
+ ],
40
+ },
41
+ "reports/site.tsx",
42
+ );
43
+ expect(report.pages.map((p) => p.id)).toEqual(["overview", "exam"]);
44
+ expect(report.title).toEqual({ en: "Memory Evals", "zh-CN": "记忆能力评测" });
45
+ expect(report.footer).toBe("Published nightly.");
46
+ });
47
+
48
+ it("content 与 pages 恰好声明一个:同给 / 同缺都报错,文案给出 <ExperimentComparison /> 下一步", () => {
49
+ for (const bad of [
50
+ { kind: "report", content: tree, pages: [{ id: "a", title: "A", content: tree }] },
51
+ { kind: "report", title: "T" },
52
+ ]) {
53
+ expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(HostReportError);
54
+ expect(() => normalizeHostReport(bad, "reports/site.tsx")).toThrow(/<ExperimentComparison \/>/);
55
+ }
56
+ });
57
+
58
+ it("空 pages 列表 / 重复 page id / 非法 page id 在装载期报错", () => {
59
+ expect(() => normalizeHostReport({ kind: "report", pages: [] }, "r.tsx")).toThrow(/non-empty/);
60
+ expect(() =>
61
+ normalizeHostReport(
62
+ { kind: "report", pages: [{ id: "exam", title: "A", content: tree }, { id: "exam", title: "B", content: tree }] },
63
+ "r.tsx",
64
+ ),
65
+ ).toThrow(/duplicate page id "exam"/);
66
+ for (const id of ["Exam", "a/b"]) {
67
+ expect(() =>
68
+ normalizeHostReport({ kind: "report", pages: [{ id, title: "A", content: tree }] }, "r.tsx"),
69
+ ).toThrow(/invalid/);
70
+ }
71
+ });
72
+
73
+ it("默认导出不是 defineReport 产物:完整用户反馈", () => {
74
+ expect(() => normalizeHostReport({ some: "object" }, "reports/bad.tsx")).toThrow(
75
+ /does not default-export a report/,
76
+ );
77
+ });
78
+
79
+ it("ReportLink.icon 是 { svg: string }:合法形状原样透传;无类型 JS 传其它形状装载报错", () => {
80
+ const svg = '<svg viewBox="0 0 16 16"><path d="M0 0h16v16z"/></svg>';
81
+ const report = normalizeHostReport(
82
+ { kind: "report", content: tree, links: [{ label: "GitHub", href: "https://example.com", icon: { svg } }] },
83
+ "reports/site.tsx",
84
+ );
85
+ expect(report.links[0]!.icon).toEqual({ svg });
86
+
87
+ // ReactNode / 组件 / 裸字符串都不是 { svg: string },装载期以完整用户反馈拒绝。
88
+ const reactNode = { $$typeof: Symbol.for("react.transitional.element"), type: "svg", props: {} };
89
+ for (const icon of [reactNode, "<svg/>", { svg: 42 }, { svg: "" }]) {
90
+ expect(() =>
91
+ normalizeHostReport(
92
+ { kind: "report", content: tree, links: [{ label: "GitHub", href: "https://example.com", icon }] },
93
+ "reports/site.tsx",
94
+ ),
95
+ ).toThrow(/icon" must be \{ svg: string \}/);
96
+ }
97
+ });
98
+
99
+ it("旧 build 函数形态(集成前桥接)恒为单页 report", () => {
100
+ const legacy = { build: () => tree };
101
+ const report = normalizeHostReport(legacy, "the built-in report");
102
+ expect(report.pages.map((p) => p.id)).toEqual(["report"]);
103
+ expect(report.pages[0]!.content).toBe(legacy);
104
+ });
105
+ });
106
+
107
+ describe("标题回退链:def.title → 唯一且相同的快照 name → 内置文案「Eval 运行结果 / Eval Results」", () => {
108
+ it("def.title 优先", () => {
109
+ expect(resolveReportTitle({ en: "T" }, [{ name: "S" }])).toEqual({ en: "T" });
110
+ });
111
+
112
+ it("无 def.title 时取 Scope 中唯一且相同(深相等,键顺序无关)的非空快照 name", () => {
113
+ expect(resolveReportTitle(undefined, [{ name: { en: "S", "zh-CN": "斯" } }, { name: { "zh-CN": "斯", en: "S" } }]))
114
+ .toEqual({ en: "S", "zh-CN": "斯" });
115
+ expect(resolveReportTitle(undefined, [{}, { name: "Only" }])).toBe("Only");
116
+ });
117
+
118
+ it("多个不同 name(en 相同、zh-CN 不同也算不同)不随机挑,落内置文案;全无 name 亦然", () => {
119
+ expect(
120
+ resolveReportTitle(undefined, [{ name: { en: "S", "zh-CN": "甲" } }, { name: { en: "S", "zh-CN": "乙" } }]),
121
+ ).toEqual(BUILT_IN_REPORT_TITLE);
122
+ expect(resolveReportTitle(undefined, [{}, {}])).toEqual(BUILT_IN_REPORT_TITLE);
123
+ expect(resolveReportTitle("", [])).toEqual(BUILT_IN_REPORT_TITLE); // 空串标题不算声明
124
+ // 链终点是内置文案(shell.md),不是产品名——品牌位固定 NiceEval,不吃标题。
125
+ expect(BUILT_IN_REPORT_TITLE).toEqual({ en: "Eval Results", "zh-CN": "Eval 运行结果" });
126
+ });
127
+
128
+ it("LocalizedText 深相等按字段值,不看键顺序", () => {
129
+ expect(localizedTextEquals({ a: "1", b: "2" }, { b: "2", a: "1" })).toBe(true);
130
+ expect(localizedTextEquals({ a: "1" }, { a: "1", b: "2" })).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe("LocalizedText 回退:locale → en → 键字典序第一个非空值", () => {
135
+ it("三级回退各自命中", () => {
136
+ expect(localizeText({ "zh-CN": "中", en: "E" }, "zh-CN")).toBe("中");
137
+ expect(localizeText({ "zh-CN": "中", en: "E" }, "fr")).toBe("E");
138
+ expect(localizeText({ "zh-TW": "繁", ja: "日" }, "en")).toBe("日"); // ja < zh-TW 字典序
139
+ expect(localizeText("plain", "en")).toBe("plain");
140
+ expect(localizeText({}, "en")).toBeUndefined();
141
+ });
142
+ });
143
+
144
+ describe("页索引与索引命令上下文", () => {
145
+ const report = normalizeHostReport(
146
+ {
147
+ kind: "report",
148
+ title: { en: "Memory Evals", "zh-CN": "记忆能力评测" },
149
+ links: [{ label: "GitHub", href: "https://example.com", icon: { svg: "<svg data-mark></svg>" } }],
150
+ pages: [
151
+ { id: "overview", title: { en: "Overview", "zh-CN": "总览" }, content: tree },
152
+ { id: "exam", title: { en: "Exam", "zh-CN": "成绩单" }, content: tree },
153
+ ],
154
+ },
155
+ "reports/site.tsx",
156
+ );
157
+
158
+ it("索引命令保留当前 --results / --report 与位置参数,复制即可复现下一层视图", () => {
159
+ const text = pageIndexText({
160
+ report,
161
+ title: "记忆能力评测",
162
+ command: { patterns: [], results: "tmp/published-results", report: "reports/site.tsx" },
163
+ locale: "zh-CN",
164
+ });
165
+ expect(text).toContain("记忆能力评测 · 2 页");
166
+ expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page overview");
167
+ expect(text).toContain("niceeval show --results tmp/published-results --report reports/site.tsx --page exam");
168
+ expect(text).toContain("总览");
169
+ expect(text).toContain("成绩单");
170
+ });
171
+
172
+ it("show 不消费 links:页索引不含 icon svg 与 href(icon 是 web 面属性)", () => {
173
+ const text = pageIndexText({
174
+ report,
175
+ title: "Memory Evals",
176
+ command: { patterns: [] },
177
+ locale: "en",
178
+ });
179
+ expect(text).not.toContain("<svg");
180
+ expect(text).not.toContain("https://example.com");
181
+ });
182
+
183
+ it("showCommand 按序携带位置参数与 --experiment / --results / --report / --page", () => {
184
+ expect(
185
+ 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");
187
+ });
188
+ });