niceeval 0.4.5 → 0.5.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 (175) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +10 -2
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/bub.ts +14 -4
  27. package/src/agents/claude-code.ts +1 -1
  28. package/src/agents/codex.ts +2 -2
  29. package/src/agents/streaming.test.ts +1 -1
  30. package/src/agents/types.ts +4 -4
  31. package/src/agents/ui-message-stream.test.ts +1 -1
  32. package/src/cli.ts +104 -14
  33. package/src/context/context.test.ts +1 -1
  34. package/src/context/context.ts +3 -3
  35. package/src/context/session.ts +2 -2
  36. package/src/context/types.ts +2 -2
  37. package/src/i18n/en.ts +29 -7
  38. package/src/i18n/zh-CN.ts +24 -2
  39. package/src/report/aggregate.ts +79 -50
  40. package/src/report/components.tsx +190 -0
  41. package/src/report/compute.ts +149 -84
  42. package/src/report/default-report.tsx +222 -0
  43. package/src/report/dual-face.test.tsx +527 -0
  44. package/src/report/format.ts +29 -0
  45. package/src/report/index.ts +79 -24
  46. package/src/report/load.ts +67 -0
  47. package/src/report/metrics.ts +2 -1
  48. package/src/report/param.ts +18 -0
  49. package/src/report/primitives.tsx +117 -0
  50. package/src/report/react/CaseList.tsx +2 -2
  51. package/src/report/react/DeltaTable.tsx +2 -2
  52. package/src/report/react/MetricBars.tsx +109 -0
  53. package/src/report/react/MetricLine.tsx +200 -0
  54. package/src/report/react/MetricMatrix.tsx +1 -1
  55. package/src/report/react/MetricScatter.tsx +1 -1
  56. package/src/report/react/MetricTable.tsx +1 -1
  57. package/src/report/react/RunOverview.tsx +4 -4
  58. package/src/report/react/Scoreboard.tsx +3 -3
  59. package/src/report/react/cell.tsx +1 -1
  60. package/src/report/react/fixtures.ts +98 -36
  61. package/src/report/react/format.ts +3 -28
  62. package/src/report/react/index.tsx +41 -24
  63. package/src/report/react/render.test.tsx +8 -8
  64. package/src/report/react/styles.css +77 -0
  65. package/src/report/report.test.ts +248 -128
  66. package/src/report/report.ts +69 -0
  67. package/src/report/text/faces.ts +356 -0
  68. package/src/report/text/layout.ts +125 -0
  69. package/src/report/text/plot.ts +127 -0
  70. package/src/report/tree.ts +220 -0
  71. package/src/report/types.ts +75 -27
  72. package/src/report/web.ts +40 -0
  73. package/src/results/copy.ts +95 -41
  74. package/src/results/format.ts +12 -11
  75. package/src/results/index.ts +34 -17
  76. package/src/results/open.ts +201 -81
  77. package/src/results/results.test.ts +595 -191
  78. package/src/results/select.ts +107 -55
  79. package/src/results/types.ts +137 -45
  80. package/src/results/writer.ts +258 -0
  81. package/src/runner/attempt.ts +3 -3
  82. package/src/runner/fingerprint.ts +1 -1
  83. package/src/runner/reporters/artifacts.ts +38 -78
  84. package/src/runner/reporters/braintrust.test.ts +2 -2
  85. package/src/runner/reporters/braintrust.ts +3 -3
  86. package/src/runner/run.ts +1 -1
  87. package/src/runner/types.ts +18 -12
  88. package/src/show/compose.ts +215 -0
  89. package/src/show/index.ts +263 -0
  90. package/src/show/render.ts +456 -0
  91. package/src/show/show.test.ts +505 -0
  92. package/src/view/app/App.tsx +85 -24
  93. package/src/view/app/components/CostScoreChart.tsx +29 -10
  94. package/src/view/app/components/ExperimentTable.tsx +27 -9
  95. package/src/view/app/components/GroupSelector.tsx +1 -1
  96. package/src/view/app/i18n.ts +18 -3
  97. package/src/view/app/lib/attempt-route.test.ts +9 -9
  98. package/src/view/app/lib/attempt-route.ts +5 -5
  99. package/src/view/app/lib/outcome.ts +1 -3
  100. package/src/view/app/lib/rows.ts +95 -15
  101. package/src/view/app/main.tsx +18 -6
  102. package/src/view/app/pages/RunsPage.tsx +4 -7
  103. package/src/view/app/pages/TracesPage.tsx +3 -8
  104. package/src/view/app/types.ts +50 -6
  105. package/src/view/client-dist/app.css +1 -1
  106. package/src/view/client-dist/app.js +20 -20
  107. package/src/view/data.test.ts +254 -0
  108. package/src/view/data.ts +336 -0
  109. package/src/view/index.ts +64 -10
  110. package/src/view/server.ts +31 -9
  111. package/src/view/shared/types.ts +47 -41
  112. package/src/view/styles.css +5 -0
  113. package/src/view/template.html +1 -0
  114. package/src/view/view-report.test.ts +308 -0
  115. package/docs-site/.mintignore +0 -13
  116. package/docs-site/AGENTS.md +0 -46
  117. package/docs-site/concepts/adapter.mdx +0 -287
  118. package/docs-site/concepts/assert.mdx +0 -243
  119. package/docs-site/concepts/drive.mdx +0 -118
  120. package/docs-site/concepts/evals.mdx +0 -108
  121. package/docs-site/concepts/experiment.mdx +0 -37
  122. package/docs-site/concepts/hitl.mdx +0 -83
  123. package/docs-site/concepts/judge.mdx +0 -129
  124. package/docs-site/concepts/overview.mdx +0 -98
  125. package/docs-site/concepts/tier.mdx +0 -42
  126. package/docs-site/docs-ref/00-index.md +0 -23
  127. package/docs-site/docs-ref/01-page-types.md +0 -43
  128. package/docs-site/docs-ref/03-style-rules.md +0 -45
  129. package/docs-site/docs-ref/04-code-examples.md +0 -37
  130. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  131. package/docs-site/docs-ref/06-checklists.md +0 -53
  132. package/docs-site/docs.json +0 -256
  133. package/docs-site/example/ai-agent-application.mdx +0 -148
  134. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  135. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  136. package/docs-site/example/showcase.mdx +0 -39
  137. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  138. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  139. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  140. package/docs-site/example/tier1-langgraph.mdx +0 -119
  141. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  142. package/docs-site/favicon.svg +0 -5
  143. package/docs-site/github-diff.css +0 -135
  144. package/docs-site/github-diff.js +0 -11
  145. package/docs-site/guides/authoring.mdx +0 -129
  146. package/docs-site/guides/ci-integration.mdx +0 -97
  147. package/docs-site/guides/connect-otel.mdx +0 -209
  148. package/docs-site/guides/connect-your-agent.mdx +0 -221
  149. package/docs-site/guides/dataset-fanout.mdx +0 -98
  150. package/docs-site/guides/experiments.mdx +0 -71
  151. package/docs-site/guides/fixtures.mdx +0 -147
  152. package/docs-site/guides/reporters.mdx +0 -113
  153. package/docs-site/guides/runner.mdx +0 -79
  154. package/docs-site/guides/sandbox-agent.mdx +0 -138
  155. package/docs-site/guides/sandbox-backends.mdx +0 -64
  156. package/docs-site/guides/scoring-guide.mdx +0 -92
  157. package/docs-site/guides/viewing-results.mdx +0 -195
  158. package/docs-site/guides/write-experiment.mdx +0 -81
  159. package/docs-site/guides/write-send.mdx +0 -347
  160. package/docs-site/index.mdx +0 -181
  161. package/docs-site/introduction.mdx +0 -141
  162. package/docs-site/quickstart.mdx +0 -136
  163. package/docs-site/reference/builtin-agents.mdx +0 -161
  164. package/docs-site/reference/capabilities.mdx +0 -76
  165. package/docs-site/reference/cli.mdx +0 -120
  166. package/docs-site/reference/define-agent.mdx +0 -168
  167. package/docs-site/reference/define-config.mdx +0 -41
  168. package/docs-site/reference/define-eval.mdx +0 -160
  169. package/docs-site/reference/events.mdx +0 -131
  170. package/docs-site/reference/expect.mdx +0 -112
  171. package/docs-site/tracker.js +0 -8
  172. package/src/report/react/data.ts +0 -17
  173. package/src/view/aggregate.ts +0 -103
  174. package/src/view/loader.test.ts +0 -61
  175. package/src/view/loader.ts +0 -257
@@ -0,0 +1,215 @@
1
+ // show 宿主的选集合成与时间轴口径(docs-site/zh/guides/viewing-results.mdx 是行为规范)。
2
+ //
3
+ // 「现刻水位」= 每个 experiment × eval 取时间上最新的那份判决,跨 run 合成:
4
+ // results.latest() 只挑「每实验最新快照」,带 eval 前缀的局部重跑会产出残缺快照;
5
+ // 榜单承诺「不会因为一次局部重跑变残缺」,所以宿主在实验的全部历史快照上逐 eval
6
+ // 向更早的 run 补齐,再把合成好的选集注入报告槽——内置默认报告与 --report 吃同一份,
7
+ // 官方榜单口径 = 宿主注入口径。本文件只消费 niceeval/results 的读取面。
8
+
9
+ import { foldEvalOutcome } from "../shared/outcome.ts";
10
+ import { evalPrefixPredicate } from "../report/aggregate.ts";
11
+ import { attemptCostUSD } from "../report/metrics.ts";
12
+ import { makeSelection } from "../results/select.ts";
13
+ import type { ResultOutcome } from "../types.ts";
14
+ import type {
15
+ AttemptHandle,
16
+ Eval,
17
+ Experiment,
18
+ Results,
19
+ Selection,
20
+ SelectionWarning,
21
+ Snapshot,
22
+ } from "../results/index.ts";
23
+
24
+ export interface ComposeOptions {
25
+ /** experiment id 前缀(--experiment),与 latest({ experiments }) 同一分段匹配语义。 */
26
+ experiment?: string;
27
+ /** eval id 前缀(位置参数),收窄选集覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围。 */
28
+ patterns?: string[];
29
+ }
30
+
31
+ /** --experiment 的实验过滤,同 results.latest({ experiments }) 的分段前缀语义。 */
32
+ export function filterExperiments(experiments: Experiment[], prefix?: string): Experiment[] {
33
+ if (prefix === undefined) return experiments;
34
+ const p = prefix.replace(/\/+$/, "");
35
+ return experiments.filter((exp) => exp.id === p || exp.id.startsWith(p + "/"));
36
+ }
37
+
38
+ /**
39
+ * 合成「现刻水位」选集:每个实验一份合成快照,快照里每道题的判决取该题最后一次
40
+ * 出现的快照(--resume 携带的复印件身份与原判决相同,取到哪份内容都一致)。
41
+ * 警告随选集重算:partial-coverage 的分母 = 已知并集 ∩ 范围;stale / synthetic
42
+ * 与 results.latest() 同口径。
43
+ */
44
+ export function composeShowSelection(results: Results, opts: ComposeOptions = {}): Selection {
45
+ const match =
46
+ opts.patterns && opts.patterns.length > 0 ? evalPrefixPredicate(opts.patterns) : () => true;
47
+ const experiments = filterExperiments(results.experiments, opts.experiment);
48
+
49
+ const snapshots: Snapshot[] = [];
50
+ const warnings: SelectionWarning[] = [];
51
+
52
+ for (const exp of experiments) {
53
+ // 逐题取最新:快照按最新在前,首个出现即最新判决
54
+ const taken = new Map<string, { ev: Eval; snapshot: Snapshot }>();
55
+ for (const snapshot of exp.snapshots) {
56
+ for (const ev of snapshot.evals) {
57
+ if (!match(ev.id) || taken.has(ev.id)) continue;
58
+ taken.set(ev.id, { ev, snapshot });
59
+ }
60
+ }
61
+ if (taken.size === 0) continue;
62
+
63
+ const picks = [...taken.values()].sort((a, b) => a.ev.id.localeCompare(b.ev.id));
64
+ let startedAt = "";
65
+ let newest: Snapshot = picks[0].snapshot;
66
+ for (const pick of picks) {
67
+ if (pick.snapshot.startedAt > startedAt) {
68
+ startedAt = pick.snapshot.startedAt;
69
+ newest = pick.snapshot;
70
+ }
71
+ }
72
+ const evals = picks.map((p) => p.ev);
73
+ const base = exp.latest;
74
+ snapshots.push({
75
+ experimentId: exp.id,
76
+ startedAt,
77
+ agent: base.agent,
78
+ ...(base.model !== undefined ? { model: base.model } : {}),
79
+ ...(base.producer ? { producer: base.producer } : {}),
80
+ schemaVersion: base.schemaVersion,
81
+ evals,
82
+ attempts: evals.flatMap((ev) => ev.attempts),
83
+ runDir: newest.runDir,
84
+ ...(base.synthetic ? { synthetic: true } : {}),
85
+ ...(base.knownEvalIds ? { knownEvalIds: [...base.knownEvalIds] } : {}),
86
+ });
87
+
88
+ // 残缺检测:跨 run 补齐后仍缺,只可能是历史上见过(或 knownEvalIds 声明过)
89
+ // 却从未在可读落盘里出现的题 —— 分母收窄到范围内,不让范围外的缺口刷屏。
90
+ const total = exp.evalIds.filter(match).length;
91
+ if (evals.length < total) {
92
+ const hint = base.synthetic
93
+ ? "re-run the experiment for a full snapshot"
94
+ : `re-run \`niceeval exp ${exp.id}\` for a full snapshot`;
95
+ warnings.push({
96
+ kind: "partial-coverage",
97
+ experimentId: exp.id,
98
+ covered: evals.length,
99
+ total,
100
+ message: `verdicts cover ${evals.length} of ${total} evals seen in history; ${hint}`,
101
+ });
102
+ }
103
+ }
104
+
105
+ let latestStartedAt = "";
106
+ for (const snapshot of snapshots) {
107
+ if (snapshot.startedAt > latestStartedAt) latestStartedAt = snapshot.startedAt;
108
+ }
109
+ for (const snapshot of snapshots) {
110
+ if (snapshot.startedAt < latestStartedAt) {
111
+ warnings.push({
112
+ kind: "stale-snapshot",
113
+ experimentId: snapshot.experimentId,
114
+ startedAt: snapshot.startedAt,
115
+ latestStartedAt,
116
+ message: `verdicts for "${snapshot.experimentId}" were produced at ${snapshot.startedAt}, before the latest run in this selection (${latestStartedAt})`,
117
+ });
118
+ }
119
+ if (snapshot.synthetic) {
120
+ warnings.push({
121
+ kind: "synthetic-experiment-id",
122
+ experimentId: snapshot.experimentId,
123
+ runDir: snapshot.runDir.dir,
124
+ message: `run "${snapshot.runDir.dir}" has results without experimentId; grouped as "${snapshot.experimentId}" by agent/model`,
125
+ });
126
+ }
127
+ }
128
+
129
+ return makeSelection(snapshots, warnings);
130
+ }
131
+
132
+ // ───────────────────────── 时间轴(--history)─────────────────────────
133
+
134
+ export interface EvalHistoryRow {
135
+ /** 该次真实执行所在 run(快照)的时刻。 */
136
+ startedAt: string;
137
+ outcome: ResultOutcome;
138
+ attempts: number;
139
+ costUSD: number | null;
140
+ /** 最新一次 attempt 的第一条失败断言("gate calledTool(...)")。 */
141
+ failedAssertion?: string;
142
+ /** errored 时的错误摘要。 */
143
+ error?: string;
144
+ }
145
+
146
+ /** attempt 的身份键(去重口径与 dedupeAttempts 一致);缺 startedAt 时不参与去重(宁可多列不误删)。 */
147
+ function attemptKey(attempt: AttemptHandle): string | undefined {
148
+ const r = attempt.result;
149
+ return r.startedAt === undefined ? undefined : `${r.attempt}|${r.startedAt}`;
150
+ }
151
+
152
+ /**
153
+ * 单 eval 的跨 run 时间轴:每次真实执行一行,新→旧。--resume 携带的复印件
154
+ * (身份键与原判决相同的条目)不占行 —— 否则趋势会被复印件灌满假数据。
155
+ */
156
+ export function evalHistory(exp: Experiment, evalId: string): EvalHistoryRow[] {
157
+ const rows: EvalHistoryRow[] = [];
158
+ const seen = new Set<string>();
159
+ // 旧→新扫描,首次出现的身份键 = 真实执行;最后整体反转成新→旧
160
+ for (const snapshot of [...exp.snapshots].reverse()) {
161
+ const ev = snapshot.evals.find((e) => e.id === evalId);
162
+ if (!ev) continue;
163
+ const fresh: AttemptHandle[] = [];
164
+ for (const attempt of ev.attempts) {
165
+ const key = attemptKey(attempt);
166
+ if (key === undefined) {
167
+ fresh.push(attempt);
168
+ continue;
169
+ }
170
+ if (seen.has(key)) continue;
171
+ seen.add(key);
172
+ fresh.push(attempt);
173
+ }
174
+ if (fresh.length === 0) continue; // 纯复印件:判决在更早的行里已经出现过
175
+ let cost: number | null = null;
176
+ for (const attempt of fresh) {
177
+ const c = attemptCostUSD(attempt.result);
178
+ if (c !== null) cost = (cost ?? 0) + c;
179
+ }
180
+ const latest = fresh[fresh.length - 1];
181
+ const failed = latest.result.assertions.find((a) => !a.passed);
182
+ rows.push({
183
+ startedAt: snapshot.startedAt,
184
+ outcome: foldEvalOutcome(fresh.map((a) => a.result)),
185
+ attempts: fresh.length,
186
+ costUSD: cost,
187
+ ...(failed ? { failedAssertion: `${failed.severity} ${failed.name}` } : {}),
188
+ ...(latest.result.error !== undefined ? { error: latest.result.error } : {}),
189
+ });
190
+ }
191
+ return rows.reverse();
192
+ }
193
+
194
+ export interface ExperimentHistoryRow {
195
+ startedAt: string;
196
+ passedEvals: number;
197
+ totalEvals: number;
198
+ costUSD: number | null;
199
+ }
200
+
201
+ /** 实验级 per-run 通过率序列(裸 `show --history`):每个快照一行,新→旧。 */
202
+ export function experimentHistory(exp: Experiment): ExperimentHistoryRow[] {
203
+ return exp.snapshots.map((snapshot) => {
204
+ let passed = 0;
205
+ for (const ev of snapshot.evals) {
206
+ if (foldEvalOutcome(ev.attempts.map((a) => a.result)) === "passed") passed += 1;
207
+ }
208
+ let cost: number | null = null;
209
+ for (const attempt of snapshot.attempts) {
210
+ const c = attemptCostUSD(attempt.result);
211
+ if (c !== null) cost = (cost ?? 0) + c;
212
+ }
213
+ return { startedAt: snapshot.startedAt, passedEvals: passed, totalEvals: snapshot.evals.length, costUSD: cost };
214
+ });
215
+ }
@@ -0,0 +1,263 @@
1
+ // niceeval show —— 终端宿主(行为规范:docs-site/zh/guides/viewing-results.mdx;
2
+ // 宿主组合语义:docs/reports.md「宿主输入的组合语义」)。
3
+ //
4
+ // 位置参数 = eval id 前缀(选「看哪些 eval」),flag 选「看哪个切面」:
5
+ // 裸跑 / 前缀 报告槽 —— 内置默认报告的 text 面(show ≡ show --report <内置默认报告>)
6
+ // 恰好一个 eval 单 eval 详情(attempt / 断言明细,宿主本体)
7
+ // --transcript / --trace / --diff[=路径] 证据切面(宿主本体):出现即走证据室,不渲染报告槽
8
+ // --history 跨 run 时间轴(内置趋势视图),与 --report 互斥
9
+ // --report <文件> 整槽换成用户报告;位置前缀 / --run / --experiment 先收窄选集再注入
10
+ // --run <目录> 结果根换成该目录;--experiment 选集只留该实验;--attempt 指定详情/证据的 attempt
11
+ //
12
+ // 数据全部走 niceeval/results 的读取面(openResults + 合成选集),不自己爬目录。
13
+
14
+ import { existsSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { openResults, type AttemptRef, type Results } from "../results/index.ts";
17
+ import { defineReport, renderReportToText, type ReportDefinition } from "../report/report.ts";
18
+ import { ReportLoadError, loadReportFile } from "../report/load.ts";
19
+ import { DefaultReport } from "../report/default-report.tsx";
20
+ import { t } from "../i18n/index.ts";
21
+ import {
22
+ composeShowSelection,
23
+ evalHistory,
24
+ experimentHistory,
25
+ filterExperiments,
26
+ } from "./compose.ts";
27
+ import {
28
+ attemptArtifactsPath,
29
+ attemptsOfEval,
30
+ diffText,
31
+ displayAttemptNumber,
32
+ evalDetailText,
33
+ evalHistoryText,
34
+ experimentHistoryText,
35
+ pickDetailAttempt,
36
+ traceText,
37
+ transcriptText,
38
+ } from "./render.ts";
39
+
40
+ export interface ShowFlags {
41
+ transcript?: boolean;
42
+ trace?: boolean;
43
+ /** --diff(文件级摘要)。 */
44
+ diff?: boolean;
45
+ /** --diff=<路径>(单个文件的完整改动;路径必须 = 连写,位置参数永远留给 eval id 前缀)。 */
46
+ diffPath?: string;
47
+ history?: boolean;
48
+ experiment?: string;
49
+ /** 人看的 1 计序号(详情块显示的 attempt 3 就传 3)。 */
50
+ attempt?: number;
51
+ run?: string;
52
+ report?: string;
53
+ }
54
+
55
+ /** 注入 IO 供测试;默认写 stdout/stderr、宽度取终端列数。 */
56
+ export interface ShowIO {
57
+ out?: (text: string) => void;
58
+ err?: (text: string) => void;
59
+ width?: number;
60
+ now?: number;
61
+ }
62
+
63
+ /** 可预期的用户错误:打一句英文直说问题与下一步,退出码 1,不抛堆栈。 */
64
+ class ShowError extends Error {}
65
+
66
+ /** 内置默认报告:报告槽的出厂填充 —— `niceeval show` ≡ `show --report <这一份>`。 */
67
+ const builtinDefaultReport: ReportDefinition = defineReport(() => ({
68
+ type: DefaultReport,
69
+ props: {},
70
+ }));
71
+
72
+ function clampWidth(columns: number | undefined): number {
73
+ if (!Number.isFinite(columns) || (columns ?? 0) <= 0) return 80;
74
+ return Math.max(40, Math.min(columns as number, 160));
75
+ }
76
+
77
+ // --report 的装载移到中性模块(两个宿主共用),show 的导出面与错误行为不变。
78
+ export { loadReportFile } from "../report/load.ts";
79
+
80
+ /** 报告里的下钻命令:AttemptRef → `niceeval show <eval id>`(查不到时退 view 深链)。 */
81
+ function makeAttemptCommand(results: Results): (ref: AttemptRef) => string {
82
+ const byRef = new Map<string, string>();
83
+ for (const run of results.runDirs) {
84
+ for (const attempt of run.attempts) {
85
+ byRef.set(`${attempt.ref.run}/${attempt.ref.result}`, attempt.evalId);
86
+ }
87
+ }
88
+ return (ref) => {
89
+ const id = byRef.get(`${ref.run}/${ref.result}`);
90
+ return id !== undefined ? `niceeval show ${id}` : `niceeval view "#/attempt/${ref.run}/${ref.result}"`;
91
+ };
92
+ }
93
+
94
+ export async function runShow(
95
+ cwd: string,
96
+ patterns: string[],
97
+ flags: ShowFlags,
98
+ io: ShowIO = {},
99
+ ): Promise<number> {
100
+ const out = io.out ?? ((text: string) => void process.stdout.write(text));
101
+ const err = io.err ?? ((text: string) => void process.stderr.write(text));
102
+ try {
103
+ await show(cwd, patterns, flags, {
104
+ out,
105
+ err,
106
+ width: clampWidth(io.width ?? process.stdout.columns),
107
+ now: io.now ?? Date.now(),
108
+ });
109
+ return 0;
110
+ } catch (e) {
111
+ if (e instanceof ShowError || e instanceof ReportLoadError) {
112
+ err(e.message.endsWith("\n") ? e.message : `${e.message}\n`);
113
+ return 1;
114
+ }
115
+ throw e;
116
+ }
117
+ }
118
+
119
+ async function show(
120
+ cwd: string,
121
+ patterns: string[],
122
+ flags: ShowFlags,
123
+ io: { out: (s: string) => void; err: (s: string) => void; width: number; now: number },
124
+ ): Promise<void> {
125
+ const evidence = flags.transcript === true || flags.trace === true || flags.diff === true || flags.diffPath !== undefined;
126
+
127
+ // 组合语义矩阵(docs/reports.md):--history 与 --report 互斥,先于任何 IO 报出来。
128
+ if (flags.history && flags.report !== undefined) {
129
+ throw new ShowError(t("cli.show.historyReportConflict"));
130
+ }
131
+
132
+ const root = flags.run !== undefined ? resolve(cwd, flags.run) : join(cwd, ".niceeval");
133
+ if (flags.run !== undefined && !existsSync(root)) {
134
+ throw new ShowError(t("cli.show.runDirMissing", { dir: root }));
135
+ }
136
+
137
+ const results = await openResults(root);
138
+ if (results.experiments.length === 0) {
139
+ const skipped =
140
+ results.skipped.length > 0
141
+ ? `\n${results.skipped.map((s) => ` skipped ${s.dir} (${s.reason})`).join("\n")}\n`
142
+ : "";
143
+ throw new ShowError(t("cli.show.noResults", { root }) + skipped);
144
+ }
145
+
146
+ if (flags.experiment !== undefined && filterExperiments(results.experiments, flags.experiment).length === 0) {
147
+ throw new ShowError(
148
+ t("cli.show.noExperimentMatch", {
149
+ arg: flags.experiment,
150
+ experiments: results.experiments.map((e) => e.id).join(", "),
151
+ }),
152
+ );
153
+ }
154
+
155
+ const selection = composeShowSelection(results, { experiment: flags.experiment, patterns });
156
+ const matchedEvalIds = [
157
+ ...new Set(selection.snapshots.flatMap((s) => s.evals.map((e) => e.id))),
158
+ ].sort();
159
+
160
+ if (patterns.length > 0 && matchedEvalIds.length === 0) {
161
+ const known = [
162
+ ...new Set(filterExperiments(results.experiments, flags.experiment).flatMap((e) => e.evalIds)),
163
+ ].sort();
164
+ throw new ShowError(
165
+ t("cli.show.noEvalMatch", { patterns: patterns.join(", "), evals: known.join(", ") || "(none)" }),
166
+ );
167
+ }
168
+
169
+ // 证据切面是宿主本体:出现即走证据室,不渲染报告槽(与默认报告同规则)。
170
+ if (evidence) {
171
+ if (matchedEvalIds.length !== 1) {
172
+ throw new ShowError(t("cli.show.evidenceNeedsEval", { matched: matchedEvalIds.length }));
173
+ }
174
+ const evalId = matchedEvalIds[0];
175
+ const attempts = attemptsOfEval(selection.snapshots, evalId);
176
+ const picked = pickDetailAttempt(attempts, flags.attempt);
177
+ if (!picked) {
178
+ throw new ShowError(
179
+ t("cli.show.attemptNotFound", {
180
+ attempt: flags.attempt ?? "?",
181
+ evalId,
182
+ available: attempts.map((a) => displayAttemptNumber(a)).join(", ") || "(none)",
183
+ }),
184
+ );
185
+ }
186
+ const header = `attempt ${displayAttemptNumber(picked)} · ${picked.experimentId} · ${picked.result.outcome}`;
187
+ const artifactPath = attemptArtifactsPath(picked, cwd);
188
+ const blocks: string[] = [];
189
+ if (flags.transcript) {
190
+ blocks.push(transcriptText({ header, events: await picked.events(), artifactPath, width: io.width }));
191
+ }
192
+ if (flags.trace) {
193
+ blocks.push(traceText({ header, spans: await picked.trace(), artifactPath, width: io.width }));
194
+ }
195
+ if (flags.diff || flags.diffPath !== undefined) {
196
+ blocks.push(diffText({ header, diff: await picked.diff(), artifactPath, file: flags.diffPath }));
197
+ }
198
+ io.out(blocks.join("\n\n") + "\n");
199
+ return;
200
+ }
201
+
202
+ // --history:内置趋势视图。时间轴只列真实执行 —— resume 携带的复印件不占行。
203
+ if (flags.history) {
204
+ const experiments = filterExperiments(results.experiments, flags.experiment);
205
+ const blocks: string[] = [];
206
+ if (patterns.length === 0) {
207
+ for (const exp of experiments) blocks.push(experimentHistoryText(exp.id, experimentHistory(exp)));
208
+ } else {
209
+ const multi = matchedEvalIds.length > 1;
210
+ for (const evalId of matchedEvalIds) {
211
+ for (const exp of experiments) {
212
+ const rows = evalHistory(exp, evalId);
213
+ if (rows.length === 0) continue;
214
+ blocks.push(evalHistoryText({ experimentId: exp.id, ...(multi ? { evalId } : {}), rows }));
215
+ }
216
+ }
217
+ }
218
+ io.out(blocks.join("\n\n") + "\n");
219
+ return;
220
+ }
221
+
222
+ // 单 eval 详情(宿主本体);--report 在场时报告槽优先,前缀只用来收窄选集。
223
+ if (flags.report === undefined && patterns.length > 0 && matchedEvalIds.length === 1) {
224
+ const evalId = matchedEvalIds[0];
225
+ const attempts = attemptsOfEval(selection.snapshots, evalId);
226
+ const detail = pickDetailAttempt(attempts, flags.attempt);
227
+ if (flags.attempt !== undefined && !detail) {
228
+ throw new ShowError(
229
+ t("cli.show.attemptNotFound", {
230
+ attempt: flags.attempt,
231
+ evalId,
232
+ available: attempts.map((a) => displayAttemptNumber(a)).join(", ") || "(none)",
233
+ }),
234
+ );
235
+ }
236
+ io.out(
237
+ evalDetailText({
238
+ evalId,
239
+ snapshots: selection.snapshots,
240
+ ...(detail ? { detail } : {}),
241
+ cwd,
242
+ now: io.now,
243
+ width: io.width,
244
+ }) + "\n",
245
+ );
246
+ return;
247
+ }
248
+
249
+ if (flags.attempt !== undefined) {
250
+ // --attempt 只对单 eval 的详情/证据生效;报告槽/榜单下无从对位。
251
+ throw new ShowError(t("cli.show.attemptNeedsEval"));
252
+ }
253
+
254
+ // 报告槽:--report 整槽替换,否则内置默认报告(同一条渲染路径)。
255
+ const definition =
256
+ flags.report !== undefined ? await loadReportFile(cwd, flags.report) : builtinDefaultReport;
257
+ const text = await renderReportToText(
258
+ definition,
259
+ { selection, results },
260
+ { width: io.width, attemptCommand: makeAttemptCommand(results) },
261
+ );
262
+ io.out(text + "\n");
263
+ }