niceeval 0.3.0 → 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 (101) hide show
  1. package/README.zh.md +11 -0
  2. package/docs/README.md +2 -1
  3. package/docs/adapters/collection.md +1 -1
  4. package/docs/adapters/contract.md +6 -5
  5. package/docs/adapters/reference/claude-code-otel-telemetry.md +1 -1
  6. package/docs/assertions.md +0 -1
  7. package/docs/capabilities-by-construction.md +2 -2
  8. package/docs/cli.md +2 -2
  9. package/docs/concepts.md +2 -2
  10. package/docs/e2e-ci.md +135 -98
  11. package/docs/eval-authoring.md +3 -3
  12. package/docs/experiments.md +7 -4
  13. package/docs/observability.md +9 -5
  14. package/docs/origin-integration.md +18 -18
  15. package/docs/references.md +1 -1
  16. package/docs/reports.md +551 -0
  17. package/docs/results-format.md +3 -3
  18. package/docs/results-lib.md +191 -0
  19. package/docs/runner.md +1 -1
  20. package/docs/sandbox.md +1 -1
  21. package/docs/source-map.md +22 -2
  22. package/docs/tier-sync.md +74 -58
  23. package/docs/view.md +39 -5
  24. package/package.json +29 -3
  25. package/src/agents/ai-sdk.ts +3 -0
  26. package/src/agents/claude-code.ts +18 -1
  27. package/src/agents/codex.ts +3 -2
  28. package/src/agents/index.ts +16 -0
  29. package/src/agents/openai-compat.test.ts +56 -0
  30. package/src/agents/openai-compat.ts +151 -0
  31. package/src/agents/types.ts +3 -1
  32. package/src/cli.ts +3 -2
  33. package/src/context/context.test.ts +78 -1
  34. package/src/context/context.ts +19 -11
  35. package/src/context/session.ts +2 -0
  36. package/src/context/types.ts +11 -5
  37. package/src/i18n/en.ts +3 -4
  38. package/src/i18n/zh-CN.ts +2 -3
  39. package/src/o11y/cost.test.ts +40 -0
  40. package/src/o11y/cost.ts +27 -5
  41. package/src/o11y/derive.ts +2 -2
  42. package/src/o11y/otlp/mappers/claude-code.test.ts +31 -0
  43. package/src/o11y/otlp/mappers/claude-code.ts +24 -0
  44. package/src/o11y/otlp/mappers/index.ts +1 -0
  45. package/src/o11y/otlp/parse.test.ts +127 -0
  46. package/src/o11y/prices.json +176 -119
  47. package/src/o11y/types.ts +2 -1
  48. package/src/report/aggregate.ts +215 -0
  49. package/src/report/compute.ts +405 -0
  50. package/src/report/format.ts +47 -0
  51. package/src/report/index.ts +40 -0
  52. package/src/report/metrics.ts +96 -0
  53. package/src/report/react/CaseList.tsx +70 -0
  54. package/src/report/react/DeltaTable.tsx +79 -0
  55. package/src/report/react/MetricMatrix.tsx +75 -0
  56. package/src/report/react/MetricScatter.tsx +217 -0
  57. package/src/report/react/MetricTable.tsx +71 -0
  58. package/src/report/react/RunOverview.tsx +87 -0
  59. package/src/report/react/Scoreboard.tsx +87 -0
  60. package/src/report/react/cell.tsx +49 -0
  61. package/src/report/react/colors.ts +42 -0
  62. package/src/report/react/data.ts +17 -0
  63. package/src/report/react/fixtures.ts +236 -0
  64. package/src/report/react/format.ts +34 -0
  65. package/src/report/react/index.tsx +34 -0
  66. package/src/report/react/render.test.tsx +303 -0
  67. package/src/report/react/styles.css +302 -0
  68. package/src/report/report.test.ts +667 -0
  69. package/src/report/types.ts +210 -0
  70. package/src/results/copy.ts +200 -0
  71. package/src/results/format.ts +75 -0
  72. package/src/results/index.ts +27 -0
  73. package/src/results/open.ts +207 -0
  74. package/src/results/results.test.ts +359 -0
  75. package/src/results/select.ts +101 -0
  76. package/src/results/types.ts +98 -0
  77. package/src/runner/attempt.ts +3 -1
  78. package/src/runner/report.test.ts +111 -0
  79. package/src/runner/report.ts +52 -1
  80. package/src/runner/reporters/artifacts.ts +1 -1
  81. package/src/runner/reporters/braintrust.test.ts +106 -0
  82. package/src/runner/reporters/braintrust.ts +197 -0
  83. package/src/runner/reporters/index.ts +3 -1
  84. package/src/runner/run.ts +30 -19
  85. package/src/runner/types.ts +17 -0
  86. package/src/sandbox/types.ts +3 -3
  87. package/src/view/app/App.tsx +82 -17
  88. package/src/view/app/components/CostScoreChart.tsx +127 -0
  89. package/src/view/app/i18n.ts +10 -1
  90. package/src/view/app/lib/attempt-route.test.ts +89 -0
  91. package/src/view/app/lib/attempt-route.ts +52 -0
  92. package/src/view/app/lib/chart.ts +72 -0
  93. package/src/view/app/lib/outcome.ts +1 -1
  94. package/src/view/app/types.ts +4 -5
  95. package/src/view/client-dist/app.css +1 -1
  96. package/src/view/client-dist/app.js +18 -18
  97. package/src/view/index.ts +12 -18
  98. package/src/view/loader.ts +14 -7
  99. package/src/view/shared/types.ts +14 -1
  100. package/src/view/styles.css +43 -0
  101. package/src/view/template.html +0 -1
package/src/view/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { copyFile, mkdir, writeFile } from "node:fs/promises";
6
6
  import { existsSync } from "node:fs";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { join, resolve } from "node:path";
8
8
  import { loadSummaries, type ScanResult } from "./loader.ts";
9
9
  import { renderHtml, type ViewOptions } from "./server.ts";
10
10
 
@@ -20,27 +20,21 @@ export {
20
20
  type SkippedRun,
21
21
  } from "./loader.ts";
22
22
 
23
- /** --out 目标是单文件(*.html)还是目录式静态导出;CLI 的提示文案也按这个口径分。 */
24
- export function isSingleFileOut(out: string): boolean {
25
- return /\.html?$/i.test(out);
26
- }
27
-
28
23
  /**
29
- * 导出静态报告(--out)。两种形态:
30
- * - `--out report.html`:单个可分享 HTML。榜单/断言/检查自包含;代码视图、transcript、
31
- * trace 依赖工件文件,单文件带不动(diff/trace 可达上百 MB),静态托管时优雅降级。
32
- * - `--out <dir>`:目录式静态导出。写 <dir>/index.html,并把前端会 fetch 的工件
33
- * (sources.json / events.json / trace.json)复制到 <dir>/artifact/<base>/——与本地
34
- * server 的 /artifact/<rel> 路由同一布局,整个目录扔给任何静态托管即是完整体验。
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「静态导出」)
35
29
  */
36
30
  export async function buildView(opts: ViewOptions = {}): Promise<string> {
37
- const scan = await loadSummaries(opts.input);
38
- const out = resolve(opts.out ?? ".niceeval/report.html");
39
- if (isSingleFileOut(out)) {
40
- await mkdir(dirname(out), { recursive: true });
41
- await writeFile(out, await renderHtml(scan), "utf-8");
42
- return out;
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
+ );
43
36
  }
37
+ const scan = await loadSummaries(opts.input);
44
38
  await mkdir(out, { recursive: true });
45
39
  await writeFile(join(out, "index.html"), await renderHtml(scan), "utf-8");
46
40
  await copyFetchedArtifacts(scan, join(out, "artifact"));
@@ -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
  }
@@ -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>