niceeval 0.6.0 → 0.6.2

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 (314) hide show
  1. package/dist/agents/types.d.ts +72 -6
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -0
  9. package/dist/report/built-ins/experiment-comparison.js +119 -0
  10. package/dist/report/built-ins/index.d.ts +2 -1
  11. package/dist/report/built-ins/index.js +2 -2
  12. package/dist/report/components.d.ts +10 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +68 -66
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -14
  19. package/dist/report/format.js +28 -30
  20. package/dist/report/index.d.ts +5 -4
  21. package/dist/report/index.js +6 -5
  22. package/dist/report/locale.d.ts +23 -3
  23. package/dist/report/locale.js +47 -6
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +66 -15
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +4 -4
  28. package/dist/report/react/AttemptList.js +8 -10
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +4 -2
  34. package/dist/report/react/ExperimentList.js +57 -7
  35. package/dist/report/react/MetricScatter.js +6 -16
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +30 -18
  40. package/dist/report/react/format.d.ts +1 -1
  41. package/dist/report/react/format.js +1 -1
  42. package/dist/report/react/index.d.ts +1 -1
  43. package/dist/report/report.d.ts +5 -1
  44. package/dist/report/report.js +6 -2
  45. package/dist/report/text/faces.d.ts +1 -1
  46. package/dist/report/text/faces.js +100 -61
  47. package/dist/report/text/table.js +36 -5
  48. package/dist/report/types.d.ts +40 -34
  49. package/dist/results/types.d.ts +11 -0
  50. package/dist/runner/feedback/sink.d.ts +110 -0
  51. package/dist/runner/types.d.ts +513 -22
  52. package/dist/sandbox/docker.d.ts +23 -2
  53. package/dist/sandbox/e2b.d.ts +15 -1
  54. package/dist/sandbox/errors.d.ts +30 -3
  55. package/dist/sandbox/io-retry.d.ts +17 -0
  56. package/dist/sandbox/registry.d.ts +2 -0
  57. package/dist/sandbox/resolve.d.ts +18 -5
  58. package/dist/sandbox/retry.d.ts +11 -1
  59. package/dist/sandbox/types.d.ts +39 -5
  60. package/dist/sandbox/vercel.d.ts +7 -1
  61. package/dist/scoring/coverage.d.ts +30 -0
  62. package/dist/scoring/display.d.ts +21 -0
  63. package/dist/scoring/display.js +120 -0
  64. package/dist/scoring/types.d.ts +103 -20
  65. package/dist/shared/aggregate.d.ts +1 -0
  66. package/dist/shared/aggregate.js +3 -3
  67. package/dist/shared/types.d.ts +28 -0
  68. package/dist/tty-line.d.ts +0 -4
  69. package/dist/util.d.ts +23 -0
  70. package/docs-site/zh/concepts/adapter.mdx +24 -6
  71. package/docs-site/zh/concepts/assert.mdx +11 -10
  72. package/docs-site/zh/concepts/evals.mdx +7 -6
  73. package/docs-site/zh/concepts/experiment.mdx +1 -1
  74. package/docs-site/zh/concepts/overview.mdx +7 -7
  75. package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
  76. package/docs-site/zh/guides/authoring.mdx +33 -0
  77. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  78. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  79. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  80. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  81. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  82. package/docs-site/zh/guides/debugging.mdx +210 -0
  83. package/docs-site/zh/guides/experiments.mdx +10 -3
  84. package/docs-site/zh/guides/fixtures.mdx +3 -1
  85. package/docs-site/zh/guides/official-adapters.mdx +27 -3
  86. package/docs-site/zh/guides/publish-report.mdx +30 -16
  87. package/docs-site/zh/guides/report-components.mdx +49 -37
  88. package/docs-site/zh/guides/reporters.mdx +2 -2
  89. package/docs-site/zh/guides/results-data.mdx +42 -8
  90. package/docs-site/zh/guides/runner.mdx +17 -7
  91. package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
  92. package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
  93. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  94. package/docs-site/zh/guides/viewing-results.mdx +85 -41
  95. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  96. package/docs-site/zh/guides/write-send.mdx +19 -2
  97. package/docs-site/zh/index.mdx +1 -1
  98. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  99. package/docs-site/zh/reference/capabilities.mdx +2 -2
  100. package/docs-site/zh/reference/cli.mdx +35 -9
  101. package/docs-site/zh/reference/define-agent.mdx +60 -5
  102. package/docs-site/zh/reference/define-config.mdx +1 -1
  103. package/docs-site/zh/reference/define-eval.mdx +42 -9
  104. package/docs-site/zh/reference/events.mdx +2 -2
  105. package/docs-site/zh/reference/expect.mdx +36 -6
  106. package/package.json +5 -1
  107. package/src/agents/ai-sdk-otel.test.ts +1 -0
  108. package/src/agents/ai-sdk.test.ts +3 -0
  109. package/src/agents/ai-sdk.ts +3 -0
  110. package/src/agents/bub-install-spec.test.ts +34 -0
  111. package/src/agents/bub-install-spec.ts +32 -0
  112. package/src/agents/bub.ts +31 -32
  113. package/src/agents/claude-code.test.ts +130 -9
  114. package/src/agents/claude-code.ts +76 -4
  115. package/src/agents/codex.test.ts +189 -40
  116. package/src/agents/codex.ts +155 -14
  117. package/src/agents/coding-cli-versions.test.ts +15 -0
  118. package/src/agents/coding-cli-versions.ts +3 -0
  119. package/src/agents/index.ts +11 -0
  120. package/src/agents/langgraph.test.ts +204 -0
  121. package/src/agents/langgraph.ts +495 -0
  122. package/src/agents/marketplace.ts +85 -0
  123. package/src/agents/native-config.test.ts +179 -0
  124. package/src/agents/native-config.ts +267 -0
  125. package/src/agents/openai-compat.test.ts +1 -0
  126. package/src/agents/openclaw.test.ts +31 -0
  127. package/src/agents/openclaw.ts +171 -0
  128. package/src/agents/plugin-config.test.ts +1 -0
  129. package/src/agents/sdk-streams.test.ts +79 -0
  130. package/src/agents/sdk-streams.ts +55 -10
  131. package/src/agents/skills.test.ts +1 -0
  132. package/src/agents/streaming.test.ts +3 -9
  133. package/src/agents/types.ts +73 -6
  134. package/src/agents/ui-message-stream.test.ts +3 -0
  135. package/src/cli.ts +411 -108
  136. package/src/context/context.test.ts +51 -12
  137. package/src/context/context.ts +161 -29
  138. package/src/context/session.test.ts +1 -0
  139. package/src/context/session.ts +114 -6
  140. package/src/context/types.ts +30 -12
  141. package/src/define.test.ts +13 -8
  142. package/src/define.ts +25 -4
  143. package/src/expect/index.ts +53 -23
  144. package/src/i18n/en.ts +65 -4
  145. package/src/i18n/zh-CN.ts +66 -4
  146. package/src/o11y/cost.test.ts +1 -0
  147. package/src/o11y/execution-tree.test.ts +1 -20
  148. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  149. package/src/o11y/otlp/parse.test.ts +1 -0
  150. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  151. package/src/o11y/parsers/bub.test.ts +1 -0
  152. package/src/o11y/parsers/claude-code.test.ts +1 -34
  153. package/src/o11y/parsers/openclaw.test.ts +154 -0
  154. package/src/o11y/parsers/openclaw.ts +310 -0
  155. package/src/o11y/prices.json +746 -311
  156. package/src/o11y/tool-names.test.ts +1 -0
  157. package/src/o11y/types.ts +16 -2
  158. package/src/report/aggregate.ts +34 -5
  159. package/src/report/built-in-user-parity.test.tsx +127 -173
  160. package/src/report/built-ins/experiment-comparison.tsx +179 -0
  161. package/src/report/built-ins/index.ts +7 -2
  162. package/src/report/components.tsx +11 -3
  163. package/src/report/compute.ts +80 -74
  164. package/src/report/dual-render.test.tsx +222 -91
  165. package/src/report/flag.ts +30 -2
  166. package/src/report/format.ts +36 -27
  167. package/src/report/index.ts +23 -6
  168. package/src/report/locale.ts +49 -6
  169. package/src/report/metrics.ts +68 -15
  170. package/src/report/primitives.tsx +6 -0
  171. package/src/report/react/AttemptList.tsx +9 -36
  172. package/src/report/react/EvalList.tsx +0 -0
  173. package/src/report/react/ExperimentComparison.tsx +68 -0
  174. package/src/report/react/ExperimentList.tsx +173 -55
  175. package/src/report/react/MetricScatter.tsx +13 -25
  176. package/src/report/react/chart-math.test.ts +85 -0
  177. package/src/report/react/chart-math.ts +101 -22
  178. package/src/report/react/enhance.js +72 -1
  179. package/src/report/react/fixtures.ts +34 -21
  180. package/src/report/react/format.ts +1 -1
  181. package/src/report/react/index.tsx +0 -1
  182. package/src/report/react/render.test.tsx +30 -69
  183. package/src/report/react/styles.css +112 -14
  184. package/src/report/report.test.ts +308 -105
  185. package/src/report/report.ts +6 -2
  186. package/src/report/text/faces.ts +111 -67
  187. package/src/report/text/table.ts +42 -5
  188. package/src/report/types.ts +42 -34
  189. package/src/results/annotated-source.test.ts +62 -9
  190. package/src/results/annotated-source.ts +64 -6
  191. package/src/results/attempt-evidence.test.ts +9 -7
  192. package/src/results/attempt-evidence.ts +15 -8
  193. package/src/results/attempt-source.ts +6 -3
  194. package/src/results/copy.ts +145 -55
  195. package/src/results/host-equivalence.test.ts +11 -9
  196. package/src/results/index.ts +2 -0
  197. package/src/results/locator.test.ts +1 -22
  198. package/src/results/open.ts +7 -1
  199. package/src/results/publish.ts +149 -0
  200. package/src/results/results.test.ts +85 -51
  201. package/src/results/truncate.ts +90 -0
  202. package/src/results/types.ts +7 -0
  203. package/src/results/writer.ts +31 -13
  204. package/src/runner/attempt.test.ts +138 -7
  205. package/src/runner/attempt.ts +603 -104
  206. package/src/runner/discover.test.ts +47 -0
  207. package/src/runner/discover.ts +36 -2
  208. package/src/runner/eval-source.test.ts +1 -27
  209. package/src/runner/feedback/agent.test.ts +504 -0
  210. package/src/runner/feedback/agent.ts +409 -0
  211. package/src/runner/feedback/ci.test.ts +562 -0
  212. package/src/runner/feedback/ci.ts +401 -0
  213. package/src/runner/feedback/coordinator.test.ts +317 -0
  214. package/src/runner/feedback/coordinator.ts +397 -0
  215. package/src/runner/feedback/failure.ts +40 -0
  216. package/src/runner/feedback/human.test.ts +616 -0
  217. package/src/runner/feedback/human.ts +535 -0
  218. package/src/runner/feedback/index.ts +66 -0
  219. package/src/runner/feedback/io.ts +78 -0
  220. package/src/runner/feedback/profile.test.ts +50 -0
  221. package/src/runner/feedback/profile.ts +58 -0
  222. package/src/runner/feedback/reducer.test.ts +395 -0
  223. package/src/runner/feedback/reducer.ts +260 -0
  224. package/src/runner/feedback/renderer.ts +82 -0
  225. package/src/runner/feedback/sink.ts +203 -0
  226. package/src/runner/feedback/testing.ts +106 -0
  227. package/src/runner/ledger.test.ts +230 -0
  228. package/src/runner/ledger.ts +329 -0
  229. package/src/runner/report.test.ts +128 -3
  230. package/src/runner/report.ts +33 -9
  231. package/src/runner/reporters/artifacts.ts +8 -2
  232. package/src/runner/reporters/braintrust.test.ts +8 -7
  233. package/src/runner/reporters/braintrust.ts +9 -2
  234. package/src/runner/reporters/index.ts +2 -2
  235. package/src/runner/reporters/json.test.ts +162 -0
  236. package/src/runner/reporters/json.ts +35 -8
  237. package/src/runner/reporters/shared.ts +1 -5
  238. package/src/runner/run.test.ts +760 -3
  239. package/src/runner/run.ts +242 -36
  240. package/src/runner/sandbox-prep.ts +3 -42
  241. package/src/runner/timing.ts +158 -0
  242. package/src/runner/types.ts +518 -22
  243. package/src/sandbox/checkpoint.test.ts +55 -0
  244. package/src/sandbox/checkpoint.ts +29 -8
  245. package/src/sandbox/cli-commands.ts +407 -0
  246. package/src/sandbox/docker.ts +115 -16
  247. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  248. package/src/sandbox/e2b-agent-template.ts +94 -0
  249. package/src/sandbox/e2b.ts +74 -9
  250. package/src/sandbox/errors.ts +111 -4
  251. package/src/sandbox/index.ts +2 -0
  252. package/src/sandbox/io-retry.test.ts +58 -0
  253. package/src/sandbox/io-retry.ts +45 -0
  254. package/src/sandbox/keep-registry.test.ts +86 -0
  255. package/src/sandbox/keep-registry.ts +142 -0
  256. package/src/sandbox/keep.ts +178 -0
  257. package/src/sandbox/paths.test.ts +1 -0
  258. package/src/sandbox/paths.ts +19 -8
  259. package/src/sandbox/registry.ts +20 -3
  260. package/src/sandbox/resolve.ts +76 -11
  261. package/src/sandbox/retry.test.ts +70 -0
  262. package/src/sandbox/retry.ts +46 -4
  263. package/src/sandbox/types.ts +44 -6
  264. package/src/sandbox/vercel.ts +43 -20
  265. package/src/scoring/collector.ts +60 -17
  266. package/src/scoring/coverage.ts +95 -0
  267. package/src/scoring/diff.ts +81 -0
  268. package/src/scoring/display.test.ts +121 -0
  269. package/src/scoring/display.ts +133 -0
  270. package/src/scoring/evidence.test.ts +189 -0
  271. package/src/scoring/judge.test.ts +142 -0
  272. package/src/scoring/judge.ts +15 -18
  273. package/src/scoring/scoped.ts +217 -50
  274. package/src/scoring/types.ts +117 -20
  275. package/src/scoring/verdict.ts +16 -4
  276. package/src/shared/aggregate.ts +3 -2
  277. package/src/shared/types.ts +31 -0
  278. package/src/show/compose.ts +2 -2
  279. package/src/show/index.ts +29 -16
  280. package/src/show/render.ts +626 -308
  281. package/src/show/show.test.ts +251 -36
  282. package/src/tty-line.ts +8 -26
  283. package/src/util.test.ts +1 -0
  284. package/src/util.ts +41 -0
  285. package/src/view/app/components/AttemptModal.tsx +153 -2
  286. package/src/view/app/components/CodeView.tsx +32 -11
  287. package/src/view/app/components/CopyControls.tsx +2 -2
  288. package/src/view/app/i18n.ts +6 -0
  289. package/src/view/app/lib/attempt-route.test.ts +1 -0
  290. package/src/view/app/lib/verdict.ts +7 -9
  291. package/src/view/artifact-serving.test.ts +2 -1
  292. package/src/view/client-dist/app.css +1 -1
  293. package/src/view/client-dist/app.js +17 -17
  294. package/src/view/data.test.ts +2 -1
  295. package/src/view/data.ts +17 -7
  296. package/src/view/index.ts +12 -1
  297. package/src/view/server.ts +2 -0
  298. package/src/view/shared/types.ts +1 -1
  299. package/src/view/styles.css +3 -0
  300. package/src/view/view-report.test.ts +11 -10
  301. package/dist/o11y/execution-tree.d.ts +0 -103
  302. package/dist/o11y/otlp/select.d.ts +0 -22
  303. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
  304. package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
  305. package/dist/results/annotated-source.d.ts +0 -61
  306. package/dist/results/attempt-evidence.d.ts +0 -69
  307. package/dist/results/attempt-source.d.ts +0 -15
  308. package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
  309. package/src/runner/reporters/console.ts +0 -70
  310. package/src/runner/reporters/live.test.ts +0 -56
  311. package/src/runner/reporters/live.ts +0 -247
  312. package/src/runner/reporters/quiet.test.ts +0 -66
  313. package/src/runner/reporters/quiet.ts +0 -49
  314. package/src/runner/reporters/table.ts +0 -277
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
1
2
  // niceeval show 终端宿主的测试(行为规范:docs-site/zh/guides/viewing-results.mdx;
2
3
  // 组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。覆盖:
3
4
  // - 榜单合成口径:每 experiment × eval 取最新判定,局部重跑从更早快照补齐,头部标注合成自几个快照;
@@ -17,7 +18,7 @@ import { tmpdir } from "node:os";
17
18
  import { join } from "node:path";
18
19
  import { createHash } from "node:crypto";
19
20
  import { openResults } from "../results/index.ts";
20
- import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
21
+ import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type TimingNode, type TraceSpan, type Verdict } from "../types.ts";
21
22
  import { selectCurrentResults } from "../results/select.ts";
22
23
  import { evalHistory } from "./compose.ts";
23
24
  import { runShow, type ShowFlags } from "./index.ts";
@@ -52,7 +53,7 @@ type AttemptFixture = Pick<EvalResult, "id" | "verdict"> &
52
53
  Partial<
53
54
  Pick<
54
55
  EvalResult,
55
- "attempt" | "durationMs" | "assertions" | "estimatedCostUSD" | "usage" | "error" | "startedAt" | "artifactBase" | "hasEvents" | "hasTrace"
56
+ "attempt" | "durationMs" | "assertions" | "estimatedCostUSD" | "usage" | "error" | "diagnostics" | "startedAt" | "artifactBase" | "hasEvents" | "hasTrace" | "phases"
56
57
  >
57
58
  >;
58
59
 
@@ -135,7 +136,7 @@ async function seedComposedRoot(): Promise<string> {
135
136
  name: 'fileChanged("src/components/Button.tsx")',
136
137
  severity: "gate",
137
138
  score: 0,
138
- passed: false,
139
+ outcome: "failed" as const,
139
140
  detail: "file was not modified",
140
141
  },
141
142
  ],
@@ -146,34 +147,31 @@ async function seedComposedRoot(): Promise<string> {
146
147
 
147
148
  // ───────────────────────── 榜单合成口径 ─────────────────────────
148
149
 
149
- describe("榜单:跨快照合成的现刻水位(show 专用 attempt 表)", () => {
150
- it("局部重跑不撕榜单:另一题从更早快照补齐,单实验不出现比较空态", async () => {
150
+ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text )", () => {
151
+ it("局部重跑不撕报告:另一题从更早快照补齐,缺成本时散点明确空态", async () => {
151
152
  const root = await seedComposedRoot();
152
153
  const { out, code } = await show(root, []);
153
154
  expect(code).toBe(0);
154
- expect(out).not.toContain("No data to plot");
155
- expect(out).not.toContain("At least 2 experiments");
155
+ expect(out).toContain("No data to plot Cost × End-to-end pass rate");
156
+ expect(out).toContain("1 point missing data");
157
+ expect(out).not.toContain("needed to compare");
156
158
  expect(out).not.toContain("COMPARISON");
157
- expect(out).toContain("EXPERIMENT compare/bub · bub");
158
- expect(out).toContain("SUMMARY 1 passed · 1 failed · 0 errored · 0 skipped · 2 attempts");
159
- expect(out).toMatch(/STATUS\s+EVAL\s+ATTEMPT\s+RESULT\s+DURATION\s+COST/);
160
- expect(out).toMatch(/✗ failed\s+fixtures\/button\s+@1[0-9a-z]{7}\s+file was not modified/);
161
- expect(out).toMatch(/✓ passed\s+weather\/brooklyn\s+@1[0-9a-z]{7}\s+—/);
162
- expect(out).not.toMatch(/@1[0-9a-z]{7}[✓✗]/);
163
- expect(out).not.toContain("[E,X,");
164
- expect(out).toContain("DRILL DOWN niceeval show @<attempt>");
159
+ expect(out).toMatch(/\bbub\s+default\s+bub\s+1s\s+50%/);
160
+ expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
161
+ expect(out).toMatch(/✗ failed\s+fixtures\/button[\s\S]*└─ @1[0-9a-z]{7}[\s\S]*fileChanged/);
162
+ expect(out).toMatch(/✓ passed\s+weather\/brooklyn[\s\S]*└─ @1[0-9a-z]{7}/);
163
+ expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
165
164
  });
166
165
 
167
- it("裸 show 使用稳定表头,不受自定义报告 locale chrome 影响", async () => {
166
+ it("裸 show 的默认报告 chrome 跟随 locale", async () => {
168
167
  const root = await seedComposedRoot();
169
168
  process.env.NICEEVAL_LANG = "zh-CN";
170
169
  try {
171
170
  const { out, code } = await show(root, []);
172
171
  expect(code).toBe(0);
173
- expect(out).toMatch(/STATUS\s+EVAL\s+ATTEMPT\s+RESULT\s+DURATION\s+COST/);
174
- expect(out).toContain("1 passed · 1 failed");
175
- expect(out).not.toContain("没有可绘制的数据");
176
- expect(out).toContain("compare/bub");
172
+ expect(out).toContain("预估成本 × 端到端成功率 没有可绘制的数据");
173
+ expect(out).toContain("1 通过 / 1 失败");
174
+ expect(out).toMatch(/\bbub\s+默认\s+bub\s+1s\s+50%/);
177
175
  } finally {
178
176
  process.env.NICEEVAL_LANG = "en";
179
177
  }
@@ -183,13 +181,13 @@ describe("榜单:跨快照合成的现刻水位(show 专用 attempt 表)", () =>
183
181
  const root = await seedComposedRoot();
184
182
  const { out, code } = await show(root, [], {}, 60);
185
183
  expect(code).toBe(0);
186
- expect(out).toContain("");
184
+ expect(out).toContain("fixtures/button");
187
185
  for (const line of out.trimEnd().split("\n")) {
188
186
  expect(stringWidth(line), line).toBeLessThanOrEqual(60);
189
187
  }
190
188
  });
191
189
 
192
- it("多个 experiment 先显示紧凑比较表,再逐 experiment 显示 attempt ", async () => {
190
+ it("多个 experiment 显示散点图,再逐 experiment Eval → Attempt", async () => {
193
191
  const root = await makeRoot();
194
192
  await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/a", agent: "codex", model: "mini", startedAt: "2026-07-08T10:00:00.000Z" }, [
195
193
  res("q1", "passed", { estimatedCostUSD: 0.1 }),
@@ -201,11 +199,13 @@ describe("榜单:跨快照合成的现刻水位(show 专用 attempt 表)", () =>
201
199
  ]);
202
200
  const { out, code } = await show(root, []);
203
201
  expect(code).toBe(0);
204
- expect(out).toMatch(/COMPARISON\nEXPERIMENT\s+AGENT\s+MODEL\s+PASS\s+FAILED\s+ERROR\s+SKIP\s+DURATION\s+COST/);
205
- expect(out).toContain("50% 1/2");
206
- expect(out).toContain("100% 2/2");
207
- expect(out.match(/^EXPERIMENT compare\//gm)).toHaveLength(2);
208
- expect(out).not.toContain("At least 2 experiments");
202
+ expect(out).toContain("better → upper right");
203
+ expect(out).toContain("A compare/a B compare/b");
204
+ expect(out).toMatch(/\bb\s+large\s+claude\s+1s\s+100%/);
205
+ expect(out).toMatch(/\ba\s+mini\s+codex\s+1s\s+50%/);
206
+ expect(out).toMatch(/✓ passed\s+q1[\s\S]*└─ @1[0-9a-z]{7}/);
207
+ expect(out).toMatch(/✗ failed\s+q2[\s\S]*└─ @1[0-9a-z]{7}/);
208
+ expect(out).not.toContain("needed to compare");
209
209
  });
210
210
 
211
211
  it("合成 Selection:每 experiment × eval 取最新判定;compose 不产生残缺警告", async () => {
@@ -281,15 +281,15 @@ describe("单 eval 详情", () => {
281
281
  name: 'calledTool("get_weather")',
282
282
  severity: "gate",
283
283
  score: 0,
284
- passed: false,
284
+ outcome: "failed" as const,
285
285
  detail: "tool was never called",
286
286
  },
287
- { name: "succeeded()", severity: "gate", score: 1, passed: true },
287
+ { name: "succeeded()", severity: "gate", score: 1, outcome: "passed" as const },
288
288
  {
289
289
  name: 'judge("回答基于实时数据")',
290
290
  severity: "soft",
291
291
  score: 0.2,
292
- passed: false,
292
+ outcome: "failed" as const,
293
293
  detail: "reply invents a temperature without any tool call",
294
294
  },
295
295
  ],
@@ -342,7 +342,7 @@ describe("--history 时间轴", () => {
342
342
  res("weather/brooklyn", "failed", {
343
343
  attempt: 1,
344
344
  estimatedCostUSD: 0.04,
345
- assertions: [{ name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false }],
345
+ assertions: [{ name: 'calledTool("get_weather")', severity: "gate", score: 0, outcome: "failed" as const }],
346
346
  }),
347
347
  ],
348
348
  );
@@ -550,7 +550,7 @@ describe("--eval", () => {
550
550
  root,
551
551
  "2026-07-08T10-00-00-000Z",
552
552
  { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
553
- [res("weather/brooklyn", "failed", { assertions: [{ name: "succeeded()", severity: "gate", score: 0, passed: false }] })],
553
+ [res("weather/brooklyn", "failed", { assertions: [{ name: "succeeded()", severity: "gate", score: 0, outcome: "failed" as const }] })],
554
554
  );
555
555
  const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
556
556
  expect(code).toBe(0);
@@ -570,11 +570,11 @@ describe("--eval", () => {
570
570
  name: 'calledTool("get_weather")',
571
571
  severity: "gate",
572
572
  score: 0,
573
- passed: false,
573
+ outcome: "failed" as const,
574
574
  detail: "tool was never called",
575
575
  loc: { file: "evals/weather/brooklyn.eval.ts", line: 2 },
576
576
  },
577
- { name: "unlocated()", severity: "soft", score: 0.5, passed: false, detail: "no loc on this one" },
577
+ { name: "unlocated()", severity: "soft", score: 0.5, outcome: "failed" as const, detail: "no loc on this one" },
578
578
  ],
579
579
  }),
580
580
  ],
@@ -600,7 +600,7 @@ describe("--eval", () => {
600
600
  it("源码行标注保留 group 与值断言的 expected/received", async () => {
601
601
  const root = await makeRoot();
602
602
  const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
603
- res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 1 } }] }),
603
+ res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, outcome: "failed" as const, expected: "4", received: "1", groupPath: ["Issue 15193"], loc: { file: "evals/manager.eval.ts", line: 1 } }] }),
604
604
  ]);
605
605
  const attemptDir = join(dir, "manager/a0");
606
606
  const content = "t.check(actual, equals(expected));\n";
@@ -619,7 +619,7 @@ describe("show @<locator>", () => {
619
619
  it("默认页直接解释失败断言的 group、期望值、实际值和源码位置", async () => {
620
620
  const root = await makeRoot();
621
621
  await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
622
- res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 40, column: 11 } }] }),
622
+ res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, outcome: "failed" as const, expected: "4", received: "1", groupPath: ["Issue 15193"], loc: { file: "evals/manager.eval.ts", line: 40, column: 11 } }] }),
623
623
  ]);
624
624
  const results = await openResults(root);
625
625
  const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
@@ -650,6 +650,41 @@ describe("show @<locator>", () => {
650
650
  expect(out).not.toContain("available:");
651
651
  });
652
652
 
653
+ it("errored attempt 首页展开结构化 error(phase/code/message/cause)与 diagnostics", async () => {
654
+ const root = await makeRoot();
655
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/claude-e2b", startedAt: "2026-07-08T10:00:00.000Z" }, [
656
+ res("agent-029", "errored", {
657
+ error: {
658
+ code: "sandbox-rate-limit",
659
+ message: "E2B sandbox allocation failed after 5 attempts",
660
+ phase: "sandbox.create",
661
+ cause: { name: "RateLimitError", message: "too many concurrent sandboxes" },
662
+ },
663
+ diagnostics: [
664
+ { code: "teardown-failed", level: "warning", message: "container stop timed out", phase: "sandbox.teardown" },
665
+ ],
666
+ }),
667
+ ]);
668
+ const results = await openResults(root);
669
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
670
+
671
+ const { out, code } = await show(root, [locator]);
672
+ expect(code).toBe(0);
673
+ expect(out).toContain(`${locator} · agent-029 · compare/claude-e2b · errored`);
674
+ // 结构化 error 块:phase 直接展示闭集点分名,code/message/cause 各一行(见 docs/feature/reports/show.md)
675
+ expect(out).toContain("error:");
676
+ expect(out).toContain("phase: sandbox.create");
677
+ expect(out).toContain("code: sandbox-rate-limit");
678
+ expect(out).toContain("message: E2B sandbox allocation failed after 5 attempts");
679
+ expect(out).toContain("cause: RateLimitError · too many concurrent sandboxes");
680
+ // attempt 级 diagnostics 块(与 verdict 独立)
681
+ expect(out).toContain("diagnostics:");
682
+ expect(out).toContain("warning · sandbox.teardown · teardown-failed");
683
+ expect(out).toContain("container stop timed out");
684
+ // errored 且无断言:不打印空的 assertions 汇总行
685
+ expect(out).not.toContain("assertions:");
686
+ });
687
+
653
688
  it("available 只逐行列出实际存在的证据命令,不打印能力字母或合并式 next", async () => {
654
689
  const root = await makeRoot();
655
690
  const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
@@ -689,6 +724,186 @@ describe("show @<locator>", () => {
689
724
  expect(out).not.toContain("available:");
690
725
  });
691
726
 
727
+ it("单独 --timing 进入完整时间树,不回落到 attempt 首页", async () => {
728
+ const root = await makeRoot();
729
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
730
+ res("weather/brooklyn", "passed", {
731
+ durationMs: 50_000,
732
+ phases: [
733
+ { name: "eval.run", durationMs: 42_000 },
734
+ { name: "sandbox.stop", durationMs: 800 },
735
+ ],
736
+ }),
737
+ ]);
738
+ const results = await openResults(root);
739
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
740
+
741
+ const { out, code } = await show(root, [locator], { timing: true });
742
+ expect(code).toBe(0);
743
+ expect(out).toContain("total 50.0s");
744
+ expect(out).toContain("eval.run 42.0s");
745
+ expect(out).toContain("teardown (not counted in total):");
746
+ expect(out).toContain("sandbox.stop 800ms");
747
+ expect(out).not.toContain("assertions:");
748
+ expect(out).not.toContain("available:");
749
+ });
750
+
751
+ it("--timing 在 80 个 detail node 内与 full 等价,81 个开始原位省略", async () => {
752
+ const root = await makeRoot();
753
+ const children: TimingNode[] = Array.from({ length: 81 }, (_, index) => ({
754
+ id: `command-${String(index).padStart(3, "0")}`,
755
+ kind: "command",
756
+ label: "tool",
757
+ startOffsetMs: index * 10,
758
+ durationMs: 81 - index,
759
+ command: { display: `tool --item=${index}` },
760
+ }));
761
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
762
+ res("weather/brooklyn", "passed", {
763
+ durationMs: 50_000,
764
+ phases: [
765
+ { name: "sandbox.create", durationMs: 1_000 },
766
+ { name: "eval.run", durationMs: 42_000, children },
767
+ { name: "sandbox.stop", durationMs: 800 },
768
+ ],
769
+ }),
770
+ ]);
771
+ const results = await openResults(root);
772
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
773
+
774
+ const bounded = await show(root, [locator], { timing: "summary" });
775
+ const full = await show(root, [locator], { timing: "full" });
776
+ expect(bounded.out).toContain("sandbox.create");
777
+ expect(bounded.out).toContain("eval.run");
778
+ expect(bounded.out).toContain("sandbox.stop");
779
+ expect(bounded.out).toMatch(/… 1 nodes omitted/);
780
+ expect(bounded.out).toContain(`niceeval show ${locator} --timing=full`);
781
+ expect(full.out).not.toContain("nodes omitted");
782
+ expect(full.out).toContain("tool --item=0");
783
+ expect(full.out).toContain("tool --item=80");
784
+
785
+ // 去掉第 81 个后,默认投影不再省略,detail 行与 full 完全一致。
786
+ children.pop();
787
+ const root80 = await makeRoot();
788
+ await writeSnapshot(root80, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
789
+ res("weather/brooklyn", "passed", {
790
+ durationMs: 50_000,
791
+ phases: [{ name: "eval.run", durationMs: 42_000, children }],
792
+ }),
793
+ ]);
794
+ const results80 = await openResults(root80);
795
+ const locator80 = results80.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
796
+ const bounded80 = await show(root80, [locator80], { timing: "summary" });
797
+ const full80 = await show(root80, [locator80], { timing: "full" });
798
+ expect(bounded80.out).toBe(full80.out);
799
+ });
800
+
801
+ it("默认投影保留失败路径/慢点/首尾,omission 报失败数且不虚构 children 合计", async () => {
802
+ const root = await makeRoot();
803
+ const children: TimingNode[] = Array.from({ length: 120 }, (_, index) => ({
804
+ id: `node-${String(index).padStart(3, "0")}`,
805
+ kind: "provider",
806
+ label: `provider-step-${index}`,
807
+ startOffsetMs: index * 100,
808
+ durationMs: index === 60 ? 99_000 : index + 1,
809
+ ...(index === 55 ? { failed: true as const } : {}),
810
+ }));
811
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
812
+ res("weather/brooklyn", "errored", {
813
+ durationMs: 120_000,
814
+ error: { code: "provider-failed", message: "boom", phase: "sandbox.create" },
815
+ phases: [{ name: "sandbox.create", durationMs: 120_000, failed: true, children }],
816
+ }),
817
+ ]);
818
+ const results = await openResults(root);
819
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
820
+ const { out } = await show(root, [locator], { timing: "summary" });
821
+
822
+ expect(out).toContain("provider-step-0");
823
+ expect(out).toContain("provider-step-55");
824
+ expect(out).toContain("provider-step-60");
825
+ expect(out).toContain("provider-step-119");
826
+ expect(out).not.toContain("combined");
827
+ });
828
+
829
+ it("operation 使用 producer label;full 按 parentSpanId 展开唯一关联 OTel 树", async () => {
830
+ const root = await makeRoot();
831
+ const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
832
+ res("weather/brooklyn", "passed", {
833
+ durationMs: 50_000,
834
+ hasTrace: true,
835
+ phases: [{
836
+ name: "workspace.diff",
837
+ durationMs: 5_000,
838
+ children: [{
839
+ id: "operation-1",
840
+ kind: "operation",
841
+ label: "export workspace diff · 1 window · 3,302 files",
842
+ startOffsetMs: 100,
843
+ durationMs: 4_000,
844
+ children: [{
845
+ id: "turn-1",
846
+ kind: "turn",
847
+ label: "s1/t1",
848
+ startOffsetMs: 200,
849
+ durationMs: 3_000,
850
+ traceId: "trace-1",
851
+ }],
852
+ }],
853
+ }],
854
+ }),
855
+ ]);
856
+ const trace: TraceSpan[] = [
857
+ { traceId: "trace-1", spanId: "child", parentSpanId: "root", name: "chat", kind: "model", startMs: 1_100, endMs: 2_000 },
858
+ { traceId: "trace-1", spanId: "root", name: "codex.exec", kind: "agent", startMs: 1_000, endMs: 3_000 },
859
+ ];
860
+ await writeFile(join(dir, "weather/brooklyn/a0/trace.json"), JSON.stringify(trace), "utf-8");
861
+ const results = await openResults(root);
862
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
863
+ const { out } = await show(root, [locator], { timing: "full" });
864
+
865
+ expect(out).toContain("operation · export workspace diff · 1 window · 3,302 files");
866
+ expect(out).toMatch(/agent · codex\.exec[\s\S]*model · chat/);
867
+ expect(out).not.toContain("git show ×");
868
+ });
869
+
870
+ it("attempt 首页 timing 只列大头,但保留变慢的 telemetry 阶段", async () => {
871
+ const root = await makeRoot();
872
+ const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
873
+ res("weather/brooklyn", "passed", {
874
+ durationMs: 100_000,
875
+ hasEvents: true,
876
+ phases: [
877
+ { name: "sandbox.queue", durationMs: 0 },
878
+ { name: "sandbox.create", durationMs: 3_000 },
879
+ { name: "workspace.baseline", durationMs: 310 },
880
+ { name: "telemetry.configure", durationMs: 358 },
881
+ { name: "eval.run", durationMs: 45_000 },
882
+ { name: "telemetry.collect", durationMs: 35_300 },
883
+ { name: "sandbox.stop", durationMs: 8_500 },
884
+ ],
885
+ }),
886
+ ]);
887
+ await writeFile(
888
+ join(dir, "weather/brooklyn/a0/events.json"),
889
+ JSON.stringify([{ type: "message", role: "assistant", text: "done" }]),
890
+ "utf-8",
891
+ );
892
+ const results = await openResults(root);
893
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
894
+
895
+ const { out } = await show(root, [locator]);
896
+ const timingLine = out.split("\n").find((line) => line.startsWith("timing:"));
897
+ expect(out).toContain("1 AI message");
898
+ expect(timingLine).toContain("sandbox.queue 0ms");
899
+ expect(timingLine).toContain("sandbox.create 3.0s");
900
+ expect(timingLine).toContain("eval.run 45.0s");
901
+ expect(timingLine).toContain("telemetry.collect 35.3s");
902
+ expect(timingLine).toContain("teardown +8.5s");
903
+ expect(timingLine).not.toContain("workspace.baseline");
904
+ expect(timingLine).not.toContain("telemetry.configure");
905
+ });
906
+
692
907
  it("语法不对的 locator 报「not a valid attempt locator」,退出码 1,不崩", async () => {
693
908
  const root = await makeRoot();
694
909
  await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
package/src/tty-line.ts CHANGED
@@ -1,32 +1,14 @@
1
- // 独立诊断行的统一终端出口。
1
+ // 独立诊断行的 bootstrap 终端出口:只服务 run 启动前的 argv/config 错误,以及崩溃兜底/
2
+ // 尚无活跃 coordinator 时的兜底(见 src/runner/feedback/sink.ts 的 no-active-coordinator 分支)。
2
3
  //
3
- // live 表格(src/runner/reporters/live.ts)重画靠 \x1B[{drawnLines}A 回跳到上一帧起点,
4
- // 这个回跳量的前提是"stderr/stdout 这块屏幕在两帧之间只有它自己在写"。sandbox teardown
5
- // 失败、budget 不可执行、reporter 抛错这类跟 attempt 进度无关的独立诊断行,如果在 live
6
- // 表格激活期间直接 process.stderr.write / console.log,就会插进两次 draw() 之间——下一帧
7
- // 按旧的 drawnLines 回跳,已经够不到表格真正的起点,清行重画变成往下多写一份,且这个偏移
8
- // 被记进新的 drawnLines,此后每帧都在错位的基础上继续错位,越滚越多(见
9
- // memory/live-raw-stderr-write-desyncs-redraw.md)。
10
- //
11
- // 所以任何不经过 Reporter.progress() / onEvalComplete() 的独立诊断行,写之前都要先广播
12
- // beforeExternalTerminalWrite(),live.ts 借此机会把已画内容清掉、drawnLines 归零,下一次
13
- // draw() 就会把这行诊断消息之下当成全新起点重画,不会累积。
14
-
15
- const listeners = new Set<() => void>();
16
-
17
- /** live.ts 用来订阅"即将有一行独立诊断消息落地"。返回取消订阅函数。 */
18
- export function onBeforeExternalTerminalWrite(fn: () => void): () => void {
19
- listeners.add(fn);
20
- return () => listeners.delete(fn);
21
- }
22
-
23
- /** 任何要绕开 Reporter 直接往终端打一行独立诊断消息的地方,写之前都先调用这个。 */
24
- export function beforeExternalTerminalWrite(): void {
25
- for (const fn of listeners) fn();
26
- }
4
+ // run 激活之后的全部反馈(dashboard、durable event、heartbeat)
5
+ // src/runner/feedback/coordinator.ts 统一排序;它通过 sink.ts 的模块级 coordinator 栈直接接管
6
+ // reportXxx() 调用的目的地(显式路由,不是广播-订阅),所以这里不再需要"写之前先广播、订阅方
7
+ // 自己清屏"的机制——那套机制是为已删除的 live.ts ANSI 回跳重画设计的(
8
+ // memory/live-raw-stderr-write-desyncs-redraw.md),新 coordinator 的 clear → append → redraw
9
+ // 顺序由 sink.ts 的显式路由保证,不依赖本文件。
27
10
 
28
11
  /** process.stderr.write 的替代:文本需自带换行(沿用现有 i18n 字符串的约定)。 */
29
12
  export function writeStderrLine(text: string): void {
30
- beforeExternalTerminalWrite();
31
13
  process.stderr.write(text);
32
14
  }
package/src/util.test.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/scoring/cases.md
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { brief, formatThrown, upsertManagedBlock } from "./util.ts";
3
4
 
package/src/util.ts CHANGED
@@ -32,6 +32,47 @@ export function formatThrown(e: unknown): string {
32
32
  return String(e);
33
33
  }
34
34
 
35
+ /**
36
+ * 截到第一个换行为止。`formatThrown()` 优先带完整 `.stack`(含本地绝对文件路径的多行调用栈)
37
+ * 给需要按 file:line 定位问题的落盘产物(`EvalResult.error`、`niceeval show`);但机器消费的
38
+ * 单行 envelope(`FailureNotice.reason`、reporter 失败诊断的 `message`……)只要「一层可行动摘要」
39
+ * ——`Error.stack` 的第一行恒为 `name: message`,不含栈帧,直接满足这个要求。完整栈仍然原样
40
+ * 留在调用方各自的落盘字段里,这个函数只负责第二次、更短的那份表达,不是唯一出口。
41
+ */
42
+ export function firstLine(text: string): string {
43
+ const idx = text.indexOf("\n");
44
+ return idx === -1 ? text : text.slice(0, idx);
45
+ }
46
+
47
+ /**
48
+ * 把 catch 到的 e 拆成 `AttemptError` 的三段:`message`(一层原因,Error.message,不带 name 前缀、
49
+ * 不拼 stack)、`stack`(完整多行栈,供 `niceeval show` 展开)、`cause`(沿 `e.cause` 取一层结构化
50
+ * 摘要 `{ name?, code?, message }`)。Node 的 `Error.stack` 不展开 cause 链,所以 cause 只能从
51
+ * `e.cause` 单独取。非 Error 值只给 message。
52
+ */
53
+ export function describeError(e: unknown): {
54
+ message: string;
55
+ stack?: string;
56
+ cause?: { name?: string; code?: string; message: string };
57
+ } {
58
+ if (!(e instanceof Error)) return { message: String(e) };
59
+ const out: { message: string; stack?: string; cause?: { name?: string; code?: string; message: string } } = {
60
+ message: e.message || e.name,
61
+ };
62
+ if (e.stack) out.stack = e.stack;
63
+ const raw = (e as { cause?: unknown }).cause;
64
+ if (raw instanceof Error) {
65
+ const c: { name?: string; code?: string; message: string } = { message: raw.message || raw.name };
66
+ if (raw.name) c.name = raw.name;
67
+ const code = (raw as { code?: unknown }).code;
68
+ if (typeof code === "string") c.code = code;
69
+ out.cause = c;
70
+ } else if (typeof raw === "string" && raw.trim() !== "") {
71
+ out.cause = { message: raw };
72
+ }
73
+ return out;
74
+ }
75
+
35
76
  /** 零填充到 4 位(数据集扇出的 id:sql/0000)。 */
36
77
  export function pad4(n: number): string {
37
78
  return String(n).padStart(4, "0");