niceeval 0.2.1 → 0.4.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 (113) hide show
  1. package/README.md +34 -34
  2. package/README.zh.md +29 -15
  3. package/docs/README.md +2 -1
  4. package/docs/adapters/collection.md +1 -1
  5. package/docs/adapters/contract.md +6 -5
  6. package/docs/adapters/reference/claude-code-otel-telemetry.md +1 -1
  7. package/docs/architecture.md +20 -20
  8. package/docs/assertions.md +0 -1
  9. package/docs/capabilities-by-construction.md +2 -2
  10. package/docs/cli.md +2 -2
  11. package/docs/concepts.md +2 -2
  12. package/docs/e2e-ci.md +135 -98
  13. package/docs/eval-authoring.md +3 -3
  14. package/docs/experiments.md +7 -4
  15. package/docs/observability.md +9 -5
  16. package/docs/origin-integration.md +18 -18
  17. package/docs/references.md +1 -1
  18. package/docs/reports.md +551 -0
  19. package/docs/results-format.md +3 -3
  20. package/docs/results-lib.md +191 -0
  21. package/docs/runner.md +1 -1
  22. package/docs/sandbox.md +1 -1
  23. package/docs/source-map.md +22 -2
  24. package/docs/tier-sync.md +74 -58
  25. package/docs/view.md +41 -4
  26. package/package.json +29 -3
  27. package/src/agents/ai-sdk.ts +3 -0
  28. package/src/agents/claude-code.ts +18 -1
  29. package/src/agents/codex.ts +3 -2
  30. package/src/agents/index.ts +16 -0
  31. package/src/agents/openai-compat.test.ts +56 -0
  32. package/src/agents/openai-compat.ts +151 -0
  33. package/src/agents/types.ts +3 -1
  34. package/src/cli.ts +2 -1
  35. package/src/context/context.test.ts +78 -1
  36. package/src/context/context.ts +19 -11
  37. package/src/context/session.ts +2 -0
  38. package/src/context/types.ts +11 -5
  39. package/src/i18n/en.ts +8 -4
  40. package/src/i18n/zh-CN.ts +6 -3
  41. package/src/o11y/cost.test.ts +40 -0
  42. package/src/o11y/cost.ts +27 -5
  43. package/src/o11y/derive.ts +2 -2
  44. package/src/o11y/otlp/mappers/claude-code.test.ts +31 -0
  45. package/src/o11y/otlp/mappers/claude-code.ts +24 -0
  46. package/src/o11y/otlp/mappers/index.ts +1 -0
  47. package/src/o11y/otlp/parse.test.ts +127 -0
  48. package/src/o11y/prices.json +176 -119
  49. package/src/o11y/types.ts +2 -1
  50. package/src/report/aggregate.ts +215 -0
  51. package/src/report/compute.ts +405 -0
  52. package/src/report/format.ts +47 -0
  53. package/src/report/index.ts +40 -0
  54. package/src/report/metrics.ts +96 -0
  55. package/src/report/react/CaseList.tsx +70 -0
  56. package/src/report/react/DeltaTable.tsx +79 -0
  57. package/src/report/react/MetricMatrix.tsx +75 -0
  58. package/src/report/react/MetricScatter.tsx +217 -0
  59. package/src/report/react/MetricTable.tsx +71 -0
  60. package/src/report/react/RunOverview.tsx +87 -0
  61. package/src/report/react/Scoreboard.tsx +87 -0
  62. package/src/report/react/cell.tsx +49 -0
  63. package/src/report/react/colors.ts +42 -0
  64. package/src/report/react/data.ts +17 -0
  65. package/src/report/react/fixtures.ts +236 -0
  66. package/src/report/react/format.ts +34 -0
  67. package/src/report/react/index.tsx +34 -0
  68. package/src/report/react/render.test.tsx +303 -0
  69. package/src/report/react/styles.css +302 -0
  70. package/src/report/report.test.ts +667 -0
  71. package/src/report/types.ts +210 -0
  72. package/src/results/copy.ts +200 -0
  73. package/src/results/format.ts +75 -0
  74. package/src/results/index.ts +27 -0
  75. package/src/results/open.ts +207 -0
  76. package/src/results/results.test.ts +359 -0
  77. package/src/results/select.ts +101 -0
  78. package/src/results/types.ts +98 -0
  79. package/src/runner/attempt.ts +3 -1
  80. package/src/runner/report.test.ts +111 -0
  81. package/src/runner/report.ts +52 -1
  82. package/src/runner/reporters/artifacts.ts +1 -1
  83. package/src/runner/reporters/braintrust.test.ts +106 -0
  84. package/src/runner/reporters/braintrust.ts +197 -0
  85. package/src/runner/reporters/console.ts +3 -3
  86. package/src/runner/reporters/index.ts +3 -1
  87. package/src/runner/reporters/live.ts +17 -5
  88. package/src/runner/reporters/shared.ts +3 -0
  89. package/src/runner/reporters/table.ts +1 -0
  90. package/src/runner/run.ts +31 -18
  91. package/src/runner/types.ts +20 -1
  92. package/src/sandbox/types.ts +3 -3
  93. package/src/shared/format.ts +8 -2
  94. package/src/view/app/App.tsx +82 -17
  95. package/src/view/app/components/AttemptModal.tsx +9 -2
  96. package/src/view/app/components/CodeView.tsx +2 -2
  97. package/src/view/app/components/CostScoreChart.tsx +127 -0
  98. package/src/view/app/components/LazyArtifact.tsx +2 -1
  99. package/src/view/app/i18n.ts +13 -1
  100. package/src/view/app/lib/artifact-url.ts +6 -0
  101. package/src/view/app/lib/attempt-route.test.ts +89 -0
  102. package/src/view/app/lib/attempt-route.ts +52 -0
  103. package/src/view/app/lib/chart.ts +72 -0
  104. package/src/view/app/lib/outcome.ts +1 -1
  105. package/src/view/app/types.ts +4 -5
  106. package/src/view/client-dist/app.css +1 -1
  107. package/src/view/client-dist/app.js +18 -18
  108. package/src/view/index.ts +36 -7
  109. package/src/view/loader.ts +14 -7
  110. package/src/view/server.ts +7 -0
  111. package/src/view/shared/types.ts +14 -1
  112. package/src/view/styles.css +43 -0
  113. package/src/view/template.html +0 -1
package/src/view/index.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  // 读取/版本判定在 loader.ts,聚合在 aggregate.ts,HTTP 与 HTML 烘焙在 server.ts,
3
3
  // server/前端共用的折叠口径、格式化与数据形状在 shared/。
4
4
 
5
- import { mkdir, writeFile } from "node:fs/promises";
6
- import { dirname, resolve } from "node:path";
7
- import { loadSummaries } from "./loader.ts";
5
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
6
+ import { existsSync } from "node:fs";
7
+ import { join, resolve } from "node:path";
8
+ import { loadSummaries, type ScanResult } from "./loader.ts";
8
9
  import { renderHtml, type ViewOptions } from "./server.ts";
9
10
 
10
11
  export { startViewServer, type ViewOptions, type ViewServer } from "./server.ts";
@@ -19,11 +20,39 @@ export {
19
20
  type SkippedRun,
20
21
  } from "./loader.ts";
21
22
 
22
- /** 导出静态 HTML 报告(--out):一次性把当前结果烘焙进单个可分享文件。 */
23
+ /**
24
+ * 导出静态报告(--out):只有目录式一种形态。写 <dir>/index.html,并把前端会 fetch 的工件
25
+ * (sources.json / events.json / trace.json)复制到 <dir>/artifact/<base>/——与本地
26
+ * server 的 /artifact/<rel> 路由同一布局,整个目录扔给任何静态托管即是完整体验。
27
+ * 单文件(*.html)导出已移除:代码/transcript/trace 视图依赖工件文件,单文件注定残缺,
28
+ * 存在本身就在诱导用户导出一份看不了证据的报告(docs/view.md「静态导出」)。
29
+ */
23
30
  export async function buildView(opts: ViewOptions = {}): Promise<string> {
31
+ const out = resolve(opts.out ?? ".niceeval/site");
32
+ if (/\.html?$/i.test(out)) {
33
+ throw new Error(
34
+ `--out expects a directory, got "${opts.out}". Single-file HTML export was removed: code, transcript and trace views need artifact files next to the page. Export a directory instead (e.g. --out site) and serve it with any static host.`,
35
+ );
36
+ }
24
37
  const scan = await loadSummaries(opts.input);
25
- const out = resolve(opts.out ?? ".niceeval/report.html");
26
- await mkdir(dirname(out), { recursive: true });
27
- await writeFile(out, await renderHtml(scan), "utf-8");
38
+ await mkdir(out, { recursive: true });
39
+ await writeFile(join(out, "index.html"), await renderHtml(scan), "utf-8");
40
+ await copyFetchedArtifacts(scan, join(out, "artifact"));
28
41
  return out;
29
42
  }
43
+
44
+ // 只复制前端会 fetch 的三类工件。diff.json / o11y.json 是运行侧产物,查看器从不读取,
45
+ // 且 diff 可达上百 MB,带进静态导出只会拖垮部署体积。
46
+ const FETCHED_ARTIFACTS = ["sources.json", "events.json", "trace.json"];
47
+
48
+ async function copyFetchedArtifacts(scan: ScanResult, artifactRoot: string): Promise<void> {
49
+ for (const [base, srcDir] of scan.artifactDirs) {
50
+ const destDir = join(artifactRoot, base);
51
+ // 输入本身已经是导出布局(比如对着上次导出的目录重新生成 index.html)时不自拷。
52
+ if (resolve(srcDir) === resolve(destDir)) continue;
53
+ const files = FETCHED_ARTIFACTS.filter((name) => existsSync(join(srcDir, name)));
54
+ if (!files.length) continue;
55
+ await mkdir(destDir, { recursive: true });
56
+ await Promise.all(files.map((name) => copyFile(join(srcDir, name), join(destDir, name))));
57
+ }
58
+ }
@@ -6,6 +6,7 @@ import { existsSync, statSync } from "node:fs";
6
6
  import { readFile, readdir, stat } from "node:fs/promises";
7
7
  import { dirname, join, relative, resolve } from "node:path";
8
8
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type RunSummary } from "../types.ts";
9
+ import type { ViewEvalResult } from "./shared/types.ts";
9
10
  import { t } from "../i18n/index.ts";
10
11
 
11
12
  export interface LoadedSummary {
@@ -112,7 +113,7 @@ export async function loadSummaries(input?: string): Promise<ScanResult> {
112
113
  if (s.isFile()) {
113
114
  // 单文件模式:这是用户明确指定的目标,任何读不了(版本/损坏/无关)都直接抛,由 CLI 打印提示退出。
114
115
  const summary = normalizeSummary(await parseSummary(target), target);
115
- return { loaded: [{ path: target, summary: withArtifactBases(summary, target, root, artifactDirs) }], skipped: [], artifactDirs };
116
+ return { loaded: [{ path: target, summary: withViewRefs(summary, target, root, artifactDirs) }], skipped: [], artifactDirs };
116
117
  }
117
118
 
118
119
  const candidates = await findSummaryFiles(target);
@@ -121,7 +122,7 @@ export async function loadSummaries(input?: string): Promise<ScanResult> {
121
122
  for (const path of candidates) {
122
123
  try {
123
124
  const summary = normalizeSummary(await parseSummary(path), path);
124
- loaded.push({ path, summary: withArtifactBases(summary, path, root, artifactDirs) });
125
+ loaded.push({ path, summary: withViewRefs(summary, path, root, artifactDirs) });
125
126
  } catch (e) {
126
127
  const entry = classifySkip(e, path);
127
128
  // 无关 JSON(NotAReportError)可以静默忽略;像报告却读不了的必须记下来。
@@ -203,25 +204,31 @@ function classifySkip(e: unknown, path: string): SkippedRun | undefined {
203
204
  }
204
205
 
205
206
  /**
206
- * 给每条 result 拼出相对 view 根的工件目录(前端据此 fetch trace.json 等)。
207
+ * 给每条 result 注入 view 侧标注:
208
+ * - attemptRef:run 目录(相对 view 根)+ summary.results 下标,`#/attempt/<run>/<result>`
209
+ * 深链的身份。必须在这里(读盘顺序)捕获下标——聚合层会把 results 重排。
210
+ * - artifactBase:相对 view 根的工件目录(前端据此 fetch trace.json 等)。
207
211
  * 返回新对象,不 mutate 读入的 summary;宿主机绝对路径只写进 artifactDirs(server 端内存),
208
212
  * 不挂到 result 上,避免随 viewData 进静态 HTML。
209
213
  */
210
- function withArtifactBases(
214
+ function withViewRefs(
211
215
  summary: RunSummary,
212
216
  summaryPath: string,
213
217
  root: string,
214
218
  artifactDirs: Map<string, string>,
215
219
  ): RunSummary {
216
220
  const runDir = dirname(summaryPath);
221
+ // 单文件入口时 run 目录就是 view 根,relative 为空串;统一占位 "."(与 classifySkip 的目录口径一致)。
222
+ const runRef = relative(root, runDir).split(/[\\/]/).join("/") || ".";
217
223
  return {
218
224
  ...summary,
219
- results: summary.results.map((r) => {
220
- if (!r.artifactsDir) return r;
225
+ results: summary.results.map((r, index): ViewEvalResult => {
226
+ const annotated: ViewEvalResult = { ...r, attemptRef: { run: runRef, result: index } };
227
+ if (!r.artifactsDir) return annotated;
221
228
  const abs = join(runDir, r.artifactsDir);
222
229
  const base = relative(root, abs).split(/[\\/]/).join("/");
223
230
  artifactDirs.set(base, abs);
224
- return { ...r, artifactBase: base };
231
+ return { ...annotated, artifactBase: base };
225
232
  }),
226
233
  };
227
234
  }
@@ -37,6 +37,13 @@ export async function startViewServer(opts: ViewOptions = {}): Promise<ViewServe
37
37
  return;
38
38
  }
39
39
  // 按需提供拆分工件(trace.json / events.json / …),前端展开时 fetch。
40
+ // 路径式 /artifact/<rel> 与目录式静态导出的文件布局一致(见 view/index.ts 的 buildView),
41
+ // 同一份前端产物在本地 server 和静态托管上用同一个相对 URL。
42
+ if (url.pathname.startsWith("/artifact/")) {
43
+ await serveArtifact(root, decodeURIComponent(url.pathname.slice("/artifact/".length)), res);
44
+ return;
45
+ }
46
+ // 兼容旧的 query 形式(0.2.x 前端烘焙的 HTML 可能还开着)。
40
47
  if (url.pathname === "/artifact") {
41
48
  await serveArtifact(root, url.searchParams.get("p") ?? "", res);
42
49
  return;
@@ -3,6 +3,19 @@
3
3
 
4
4
  import type { EvalResult, LocalizedText, Usage } from "../../types.ts";
5
5
 
6
+ /**
7
+ * attempt 的深链身份:run 目录(相对 view 输入根,通常就是 `.niceeval/` 下的时间戳目录名)
8
+ * + 该 attempt 在 summary.results 里的下标。`#/attempt/<run>/<result>` 路由的参数——
9
+ * 报告页(前门)与 view(证据室)靠这份契约指向同一个 attempt,见 docs/view.md「用 Reports 积木重建 view」。
10
+ */
11
+ export interface AttemptRef {
12
+ run: string;
13
+ result: number;
14
+ }
15
+
16
+ /** view 侧的 attempt 结果 = 瘦身后的 EvalResult + loadSummaries 注入的深链身份。 */
17
+ export type ViewEvalResult = EvalResult & { attemptRef?: AttemptRef };
18
+
6
19
  /** 榜单一行 = 一个实验(或 legacy 的 agent×model 组合),跨全部历史 run 聚合。 */
7
20
  export interface ViewRow {
8
21
  key: string;
@@ -27,7 +40,7 @@ export interface ViewRow {
27
40
  estimatedCostUSD?: number;
28
41
  /** 该实验组里最新一次 run 的 startedAt(ISO);详情展示「运行时间」。 */
29
42
  lastRunAt?: string;
30
- results: EvalResult[];
43
+ results: ViewEvalResult[];
31
44
  }
32
45
 
33
46
  /** 目录扫描里被跳过的 run 在页面顶部的提示条目。 */
@@ -34,6 +34,13 @@
34
34
  /* send/回复行的强调色,对齐 landing page setup-card 的 --blue(#6ea8fe)。 */
35
35
  --blue: light-dark(#1d63d8, #6ea8fe);
36
36
  --focus: light-dark(#111113, #ededed);
37
+ /* 分类色板(cost/score 散点图按实验区分系列),六色顺序按 CVD 校验过,不要重排。 */
38
+ --series-1: light-dark(#2a78d6, #3987e5);
39
+ --series-2: light-dark(#1baf7a, #199e70);
40
+ --series-3: light-dark(#eda100, #c98500);
41
+ --series-4: light-dark(#008300, #008300);
42
+ --series-5: light-dark(#e34948, #e66767);
43
+ --series-6: light-dark(#eb6834, #d95926);
37
44
  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
38
45
  }
39
46
 
@@ -308,6 +315,42 @@ tbody tr:last-child td { border-bottom: 0; }
308
315
  .group-card-rate { font-size: 30px; font-weight: 760; font-variant-numeric: tabular-nums; line-height: 1; margin-bottom: 6px; }
309
316
  .group-card-meta { font-size: 12px; color: var(--muted); }
310
317
  .group-card-time { font-size: 11px; color: var(--soft); margin-top: 2px; }
318
+ .cost-score-chart {
319
+ border: 1px solid var(--line);
320
+ border-radius: 8px;
321
+ padding: 16px 20px 10px;
322
+ margin-bottom: 20px;
323
+ background: var(--panel);
324
+ }
325
+ .csc-head { font-size: 13px; font-weight: 680; color: var(--muted); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
326
+ .csc-svg-wrap { position: relative; width: 100%; }
327
+ .csc-svg { width: 100%; height: auto; display: block; overflow: visible; }
328
+ .csc-grid line { stroke: var(--line); stroke-width: 1; }
329
+ .csc-axis text { fill: var(--muted); font-size: 10px; }
330
+ .csc-point { cursor: pointer; }
331
+ .csc-point .csc-hit { fill: transparent; }
332
+ .csc-point .csc-dot { stroke: var(--panel); stroke-width: 2; }
333
+ .csc-point text { fill: var(--text); font-size: 11px; }
334
+ .csc-leader { stroke: var(--line-strong); stroke-width: 1; }
335
+ .csc-point:hover .csc-dot,
336
+ .csc-point:focus-visible .csc-dot { r: 6.5; }
337
+ .csc-point:focus-visible { outline: none; }
338
+ .csc-tooltip {
339
+ position: absolute;
340
+ transform: translate(-50%, -100%);
341
+ margin-top: -12px;
342
+ background: var(--panel);
343
+ border: 1px solid var(--line-strong);
344
+ border-radius: 6px;
345
+ padding: 8px 10px;
346
+ font-size: 12px;
347
+ white-space: nowrap;
348
+ pointer-events: none;
349
+ box-shadow: 0 4px 16px color-mix(in oklch, var(--text), transparent 88%);
350
+ z-index: 5;
351
+ }
352
+ .csc-tooltip b { font-size: 13px; }
353
+ .csc-tooltip .csc-tooltip-meta { color: var(--muted); margin-top: 2px; }
311
354
  .section-sub-head {
312
355
  display: flex;
313
356
  align-items: center;
@@ -9,7 +9,6 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
12
- <script src="https://unpkg.com/grab/dist/index.global.js"></script>
13
12
  <script>new Function(__NICEEVAL_APP_CODE__)();</script>
14
13
  </body>
15
14
  </html>