niceeval 0.1.1 → 0.1.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 (133) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +13 -13
  3. package/docs/README.md +119 -0
  4. package/docs/adapters/README.md +60 -0
  5. package/docs/adapters/authoring.md +179 -0
  6. package/docs/adapters/coding-agent-skills-plugins.md +324 -0
  7. package/docs/adapters/collection.md +128 -0
  8. package/docs/adapters/contract.md +263 -0
  9. package/docs/adapters/reference/agent-eval.md +215 -0
  10. package/docs/adapters/reference/agent-loop-apis.md +69 -0
  11. package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
  12. package/docs/adapters/reference/eve-protocol.md +127 -0
  13. package/docs/adapters/reference/otel-genai.md +107 -0
  14. package/docs/adapters/reference/otel-instrumentation.md +65 -0
  15. package/docs/adapters/targets.md +99 -0
  16. package/docs/architecture.md +140 -0
  17. package/docs/assertions.md +388 -0
  18. package/docs/capabilities-by-construction.md +48 -0
  19. package/docs/cli.md +171 -0
  20. package/docs/concepts.md +106 -0
  21. package/docs/e2e-ci.md +295 -0
  22. package/docs/eval-authoring.md +319 -0
  23. package/docs/experiments.md +154 -0
  24. package/docs/getting-started.md +224 -0
  25. package/docs/multi-agent.md +145 -0
  26. package/docs/observability.md +333 -0
  27. package/docs/origin-integration.md +195 -0
  28. package/docs/references.md +35 -0
  29. package/docs/results-format.md +228 -0
  30. package/docs/runner.md +104 -0
  31. package/docs/sandbox.md +276 -0
  32. package/docs/scoring.md +119 -0
  33. package/docs/source-map.md +106 -0
  34. package/docs/tier-sync.md +177 -0
  35. package/docs/view.md +157 -0
  36. package/docs/vision.md +88 -0
  37. package/package.json +35 -5
  38. package/src/agents/ai-sdk-otel.ts +0 -0
  39. package/src/agents/ai-sdk.test.ts +377 -0
  40. package/src/agents/ai-sdk.ts +577 -0
  41. package/src/agents/bub.ts +30 -37
  42. package/src/agents/claude-code.ts +7 -4
  43. package/src/agents/codex.ts +15 -10
  44. package/src/agents/index.ts +43 -1
  45. package/src/agents/sdk-streams.test.ts +145 -0
  46. package/src/agents/sdk-streams.ts +457 -0
  47. package/src/agents/shared.ts +77 -39
  48. package/src/agents/streaming.test.ts +152 -0
  49. package/src/agents/streaming.ts +195 -0
  50. package/src/agents/types.ts +218 -0
  51. package/src/agents/ui-message-stream.test.ts +249 -0
  52. package/src/agents/ui-message-stream.ts +372 -0
  53. package/src/cli.ts +124 -77
  54. package/src/context/context.test.ts +203 -7
  55. package/src/context/context.ts +158 -49
  56. package/src/context/session.test.ts +96 -0
  57. package/src/context/session.ts +119 -9
  58. package/src/context/types.ts +205 -0
  59. package/src/define.ts +4 -14
  60. package/src/expect/index.ts +8 -71
  61. package/src/i18n/core.ts +28 -0
  62. package/src/i18n/en.ts +47 -5
  63. package/src/i18n/index.ts +5 -17
  64. package/src/i18n/zh-CN.ts +47 -5
  65. package/src/index.ts +12 -0
  66. package/src/loaders/index.ts +8 -39
  67. package/src/o11y/otlp/canonical.ts +7 -6
  68. package/src/o11y/otlp/mappers/codex.ts +10 -8
  69. package/src/o11y/otlp/mappers/index.ts +9 -21
  70. package/src/o11y/otlp/receiver.ts +25 -7
  71. package/src/o11y/otlp/sandbox-receiver.ts +57 -21
  72. package/src/o11y/otlp/turn-otel.test.ts +110 -0
  73. package/src/o11y/otlp/turn-otel.ts +125 -0
  74. package/src/o11y/parsers/bub.ts +13 -11
  75. package/src/o11y/parsers/claude-code.ts +27 -51
  76. package/src/o11y/parsers/codex.ts +29 -46
  77. package/src/o11y/parsers/index.ts +5 -14
  78. package/src/o11y/tool-names.test.ts +61 -0
  79. package/src/o11y/tool-names.ts +74 -0
  80. package/src/o11y/types.ts +135 -0
  81. package/src/runner/attempt.ts +456 -0
  82. package/src/runner/fingerprint.ts +59 -0
  83. package/src/runner/remote-sandbox.ts +53 -0
  84. package/src/runner/report.ts +53 -0
  85. package/src/runner/reporters/artifacts.ts +25 -4
  86. package/src/runner/reporters/console.ts +2 -8
  87. package/src/runner/reporters/live.ts +2 -8
  88. package/src/runner/reporters/shared.ts +15 -0
  89. package/src/runner/reporters/table.ts +10 -62
  90. package/src/runner/run.ts +114 -619
  91. package/src/runner/types.ts +237 -0
  92. package/src/sandbox/checkpoint.ts +15 -13
  93. package/src/sandbox/docker-stream.ts +87 -0
  94. package/src/sandbox/docker.ts +42 -120
  95. package/src/sandbox/e2b.ts +24 -75
  96. package/src/sandbox/local-files.ts +33 -0
  97. package/src/sandbox/paths.test.ts +85 -0
  98. package/src/sandbox/paths.ts +45 -0
  99. package/src/sandbox/resolve.ts +10 -34
  100. package/src/sandbox/shell.ts +17 -0
  101. package/src/sandbox/source-files.ts +42 -2
  102. package/src/sandbox/types.ts +158 -0
  103. package/src/sandbox/vercel.ts +55 -71
  104. package/src/scoring/collector.ts +3 -2
  105. package/src/scoring/judge.ts +72 -146
  106. package/src/scoring/match.ts +79 -0
  107. package/src/scoring/types.ts +76 -0
  108. package/src/shared/aggregate.ts +48 -0
  109. package/src/shared/format.ts +20 -0
  110. package/src/shared/outcome.ts +48 -0
  111. package/src/shared/types.ts +37 -0
  112. package/src/types.ts +20 -911
  113. package/src/view/aggregate.ts +103 -0
  114. package/src/view/app/App.tsx +26 -5
  115. package/src/view/app/components/AttemptModal.tsx +12 -3
  116. package/src/view/app/components/CodeView.tsx +9 -5
  117. package/src/view/app/components/CopyControls.tsx +4 -4
  118. package/src/view/app/components/ExperimentTable.tsx +5 -5
  119. package/src/view/app/i18n.ts +31 -7
  120. package/src/view/app/lib/format.ts +17 -20
  121. package/src/view/app/lib/outcome.ts +4 -16
  122. package/src/view/app/main.tsx +3 -5
  123. package/src/view/app/pages/RunsPage.tsx +2 -2
  124. package/src/view/app/pages/TracesPage.tsx +2 -2
  125. package/src/view/app/shared.ts +2 -1
  126. package/src/view/app/types.ts +6 -43
  127. package/src/view/client-dist/app.css +1 -1
  128. package/src/view/client-dist/app.js +18 -18
  129. package/src/view/index.ts +24 -401
  130. package/src/view/loader.ts +234 -0
  131. package/src/view/server.ts +136 -0
  132. package/src/view/shared/types.ts +64 -0
  133. package/src/view/styles.css +60 -9
@@ -0,0 +1,136 @@
1
+ // HTTP server 与静态资源:起本地 web、按需吐工件、把 viewData 烘焙进单个 HTML。
2
+ // 数据读取在 loader.ts,聚合在 aggregate.ts;这里只管「怎么送到浏览器」。
3
+
4
+ import { createServer, type Server } from "node:http";
5
+ import { readFile } from "node:fs/promises";
6
+ import { resolve } from "node:path";
7
+ import { loadSummaries, viewRoot, type ScanResult } from "./loader.ts";
8
+ import { buildViewData } from "./aggregate.ts";
9
+ import { formatThrown } from "../util.ts";
10
+
11
+ export interface ViewOptions {
12
+ input?: string;
13
+ out?: string;
14
+ port?: number;
15
+ }
16
+
17
+ export interface ViewServer {
18
+ url: string;
19
+ close(): Promise<void>;
20
+ }
21
+
22
+ const TEMPLATE_PLACEHOLDERS = {
23
+ styles: "<!-- __NICEEVAL_STYLES__ -->",
24
+ appCode: "__NICEEVAL_APP_CODE__",
25
+ viewData: "__NICEEVAL_VIEW_DATA_JSON__",
26
+ } as const;
27
+
28
+ export async function startViewServer(opts: ViewOptions = {}): Promise<ViewServer> {
29
+ const input = opts.input;
30
+ const root = viewRoot(input);
31
+ const server = createServer(async (req, res) => {
32
+ try {
33
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
34
+ if (url.pathname === "/healthz") {
35
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
36
+ res.end("ok");
37
+ return;
38
+ }
39
+ // 按需提供拆分工件(trace.json / events.json / …),前端展开时 fetch。
40
+ if (url.pathname === "/artifact") {
41
+ await serveArtifact(root, url.searchParams.get("p") ?? "", res);
42
+ return;
43
+ }
44
+ if (url.pathname !== "/") {
45
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
46
+ res.end("not found");
47
+ return;
48
+ }
49
+ res.writeHead(200, {
50
+ "content-type": "text/html; charset=utf-8",
51
+ "cache-control": "no-store",
52
+ });
53
+ res.end(await renderHtml(await loadSummaries(input)));
54
+ } catch (e) {
55
+ res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
56
+ res.end(formatThrown(e));
57
+ }
58
+ });
59
+
60
+ const port = await listen(server, opts.port ?? 0);
61
+ return {
62
+ url: `http://127.0.0.1:${port}/`,
63
+ close: () =>
64
+ new Promise((resolveClose, reject) => {
65
+ server.close((err) => (err ? reject(err) : resolveClose()));
66
+ }),
67
+ };
68
+ }
69
+
70
+ /** 把 viewData(只含原始值与相对路径,不含宿主机绝对路径)和前端产物烘焙进单个 HTML。 */
71
+ export async function renderHtml(scan: ScanResult): Promise<string> {
72
+ const template = await readViewAsset("template.html");
73
+ const styles = await readViewAsset("client-dist/app.css");
74
+ const app = await readViewAsset("client-dist/app.js");
75
+ const viewData = buildViewData(scan);
76
+
77
+ return template
78
+ .replace(TEMPLATE_PLACEHOLDERS.styles, () => `<style>\n${styles}\n</style>`)
79
+ .replace(TEMPLATE_PLACEHOLDERS.viewData, () => JSON.stringify(viewData).replace(/</g, "\\u003c"))
80
+ .replace(TEMPLATE_PLACEHOLDERS.appCode, () => JSON.stringify(app).replace(/</g, "\\u003c"));
81
+ }
82
+
83
+ /** 安全地把 root 下的工件文件吐回去(限定 .json,且解析后必须仍在 root 内)。 */
84
+ async function serveArtifact(
85
+ root: string,
86
+ rel: string,
87
+ res: import("node:http").ServerResponse,
88
+ ): Promise<void> {
89
+ const abs = resolve(root, rel);
90
+ const within = abs === root || abs.startsWith(root + "/");
91
+ if (!within || !rel.endsWith(".json")) {
92
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
93
+ res.end("bad artifact path");
94
+ return;
95
+ }
96
+ try {
97
+ const body = await readFile(abs, "utf-8");
98
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
99
+ res.end(body);
100
+ } catch {
101
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
102
+ res.end("artifact not found");
103
+ }
104
+ }
105
+
106
+ async function readViewAsset(name: string): Promise<string> {
107
+ return readFile(new URL(name, import.meta.url), "utf-8");
108
+ }
109
+
110
+ async function listen(server: Server, preferredPort: number): Promise<number> {
111
+ const tryListen = (port: number): Promise<number> =>
112
+ new Promise((resolveListen, reject) => {
113
+ const onError = (err: NodeJS.ErrnoException) => {
114
+ server.off("listening", onListening);
115
+ reject(err);
116
+ };
117
+ const onListening = () => {
118
+ server.off("error", onError);
119
+ const address = server.address();
120
+ resolveListen(typeof address === "object" && address ? address.port : port);
121
+ };
122
+ server.once("error", onError);
123
+ server.once("listening", onListening);
124
+ server.listen(port, "127.0.0.1");
125
+ });
126
+
127
+ if (preferredPort === 0) return tryListen(0);
128
+ for (let port = preferredPort; port < preferredPort + 20; port++) {
129
+ try {
130
+ return await tryListen(port);
131
+ } catch (e) {
132
+ if ((e as NodeJS.ErrnoException).code !== "EADDRINUSE") throw e;
133
+ }
134
+ }
135
+ throw new Error(`No available port near ${preferredPort}`);
136
+ }
@@ -0,0 +1,64 @@
1
+ // server(aggregate/loader)与前端(app/)共用的 view 数据形状。
2
+ // viewData 会被序列化进静态 HTML,两边必须对同一份声明编程;只允许 type import。
3
+
4
+ import type { EvalResult, LocalizedText, Usage } from "../../types.ts";
5
+
6
+ /** 榜单一行 = 一个实验(或 legacy 的 agent×model 组合),跨全部历史 run 聚合。 */
7
+ export interface ViewRow {
8
+ key: string;
9
+ experimentId?: string;
10
+ experiment?: EvalResult["experiment"];
11
+ group?: string;
12
+ label: string;
13
+ agent: string;
14
+ model?: string;
15
+ /** 总 attempt 数(详情里作次要信息)。 */
16
+ runs: number;
17
+ /** 去重后的 eval 数(成功率分母的口径)。 */
18
+ evals: number;
19
+ passed: number;
20
+ failed: number;
21
+ errored: number;
22
+ skipped: number;
23
+ scored?: number;
24
+ passRate: number;
25
+ avgDurationMs: number;
26
+ usage: Usage;
27
+ estimatedCostUSD?: number;
28
+ /** 该实验组里最新一次 run 的 startedAt(ISO);详情展示「运行时间」。 */
29
+ lastRunAt?: string;
30
+ results: EvalResult[];
31
+ }
32
+
33
+ /** 目录扫描里被跳过的 run 在页面顶部的提示条目。 */
34
+ export interface SkippedRunNotice {
35
+ /** run 目录(summary.json 所在目录),相对 cwd。 */
36
+ dir: string;
37
+ reason: "incompatible-version" | "malformed";
38
+ schemaVersion?: number;
39
+ producerVersion?: string;
40
+ /** incompatible-version:服务端拼好的查看命令(npx niceeval@<version> view <dir>)。 */
41
+ command?: string;
42
+ /** malformed:一句诊断(invalid JSON / results 不是数组 …)。 */
43
+ detail?: string;
44
+ }
45
+
46
+ /**
47
+ * 烘焙进 HTML 的页面数据。时间/比率/成本一律传原始值(ISO 字符串、number),
48
+ * 格式化统一由前端按当前界面 locale 做,server 不预格式化。
49
+ */
50
+ export interface ViewData {
51
+ rows?: ViewRow[];
52
+ /** 项目名(来自 config.name);hero 标题,可按 locale 多语言。 */
53
+ name?: LocalizedText;
54
+ /** 最近一次 run 的 startedAt(ISO);没有历史 run 时缺省。 */
55
+ lastRunAt?: string;
56
+ /** 0–1 的通过率。 */
57
+ passRate: number;
58
+ /** 去重后的 eval 结果数。 */
59
+ resultCount: number;
60
+ durationMs: number;
61
+ estimatedCostUSD?: number;
62
+ /** 读不了 / 版本不同而被跳过的 run;前端顶部横幅展示。 */
63
+ skippedRuns?: SkippedRunNotice[];
64
+ }
@@ -14,6 +14,7 @@
14
14
  --color-good: var(--good);
15
15
  --color-bad: var(--bad);
16
16
  --color-warn: var(--warn);
17
+ --color-blue: var(--blue);
17
18
  --color-focus: var(--focus);
18
19
  }
19
20
 
@@ -30,6 +31,8 @@
30
31
  --good: light-dark(#087f5b, #3ddc97);
31
32
  --bad: light-dark(#b42318, #ff6b6b);
32
33
  --warn: light-dark(#9a6700, #e8b84a);
34
+ /* send/回复行的强调色,对齐 landing page setup-card 的 --blue(#6ea8fe)。 */
35
+ --blue: light-dark(#1d63d8, #6ea8fe);
33
36
  --focus: light-dark(#111113, #ededed);
34
37
  font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
35
38
  }
@@ -170,6 +173,40 @@ h1 {
170
173
  font-variant-numeric: tabular-nums;
171
174
  white-space: nowrap;
172
175
  }
176
+ /* schemaVersion 不匹配的 run:榜单不解析它们,这里占位并给出 npx 查看命令。 */
177
+ .incompatible-banner {
178
+ border: 1px solid color-mix(in oklch, var(--warn), var(--line) 45%);
179
+ border-left: 3px solid var(--warn);
180
+ border-radius: 8px;
181
+ background: color-mix(in oklch, var(--warn), var(--panel) 92%);
182
+ padding: 12px 16px;
183
+ margin: 0 auto 36px;
184
+ font-size: 13px;
185
+ }
186
+ .incompatible-banner ul {
187
+ margin: 8px 0 0;
188
+ padding: 0;
189
+ list-style: none;
190
+ display: grid;
191
+ gap: 6px;
192
+ }
193
+ .incompatible-banner li {
194
+ display: flex;
195
+ flex-wrap: wrap;
196
+ align-items: baseline;
197
+ gap: 4px 12px;
198
+ }
199
+ .incompatible-banner .ib-dir { font-weight: 600; }
200
+ .incompatible-banner .ib-meta { color: var(--soft); }
201
+ .incompatible-banner code {
202
+ font-family: var(--mono, ui-monospace, monospace);
203
+ background: color-mix(in oklch, var(--panel-2), var(--panel) 40%);
204
+ border: 1px solid var(--line);
205
+ border-radius: 5px;
206
+ padding: 1px 7px;
207
+ user-select: all;
208
+ }
209
+
173
210
  .section-head {
174
211
  display: flex;
175
212
  align-items: end;
@@ -913,8 +950,16 @@ a.brand:hover { opacity: 0.75; }
913
950
  }
914
951
  .code-line.line-clickable { cursor: pointer; }
915
952
  .code-line.line-clickable:hover { background: color-mix(in oklch, var(--panel), var(--line) 45%); }
916
- .code-line.line-clickable:hover .gchev { color: var(--text); }
917
- .code-line.line-send { border-left-color: var(--line-strong); }
953
+ /* send 行学 landing setup-card reply 行:蓝底 + 蓝左缘 + 蓝行号,和断言行的绿/红/黄一眼区分。 */
954
+ .code-line.line-send {
955
+ background: color-mix(in oklch, var(--blue), var(--panel) 88%);
956
+ border-left-color: var(--blue);
957
+ }
958
+ .code-line.line-send .ln {
959
+ background: color-mix(in oklch, var(--blue), var(--panel) 74%);
960
+ color: color-mix(in oklch, var(--blue), var(--text) 45%);
961
+ }
962
+ .code-line.line-send.line-clickable:hover { background: color-mix(in oklch, var(--blue), var(--panel) 82%); }
918
963
  .code-line.is-open { background: color-mix(in oklch, var(--panel), var(--line) 34%); }
919
964
 
920
965
  /* github-diff 式整行红绿:行底色 + 更深的行号格 + 实色左缘(过=绿/加,不过=红/减)。 */
@@ -959,12 +1004,12 @@ a.brand:hover { opacity: 0.75; }
959
1004
  justify-content: flex-end;
960
1005
  }
961
1006
  .gmark { display: inline-flex; align-items: center; justify-content: center; }
962
- .gchev { width: 12px; height: 12px; color: var(--muted); transition: transform 0.12s; }
963
- .gchev.is-open { transform: rotate(90deg); }
964
- .gstat { font-size: 13px; font-weight: 800; line-height: 1; }
1007
+ /* gutter lucide 图标(对齐 landing:CheckCircle2/MessageCircle),不再用 ✓/✗ 文本字形。 */
1008
+ .gstat { width: 13px; height: 13px; flex-shrink: 0; }
965
1009
  .gstat.good { color: var(--good); }
966
1010
  .gstat.bad { color: var(--bad); }
967
1011
  .gstat.warn { color: var(--warn); }
1012
+ .gsend { width: 13px; height: 13px; flex-shrink: 0; color: var(--blue); }
968
1013
 
969
1014
  .ctext { white-space: pre; overflow-x: auto; color: var(--text); padding-left: 8px; }
970
1015
  .ctext::-webkit-scrollbar { height: 0; }
@@ -996,10 +1041,15 @@ a.brand:hover { opacity: 0.75; }
996
1041
  background: color-mix(in oklch, var(--panel), var(--line) 28%);
997
1042
  transition: color 0.12s, border-color 0.12s, background 0.12s;
998
1043
  }
1044
+ .code-line.line-send .reply-hint {
1045
+ color: color-mix(in oklch, var(--blue), var(--text) 30%);
1046
+ border-color: color-mix(in oklch, var(--blue), var(--line) 55%);
1047
+ background: color-mix(in oklch, var(--blue), transparent 88%);
1048
+ }
999
1049
  .code-line.line-clickable:hover .reply-hint {
1000
1050
  color: var(--text);
1001
- border-color: var(--line-strong);
1002
- background: color-mix(in oklch, var(--panel), var(--line) 52%);
1051
+ border-color: color-mix(in oklch, var(--blue), var(--line) 30%);
1052
+ background: color-mix(in oklch, var(--blue), transparent 80%);
1003
1053
  }
1004
1054
  .line-chev {
1005
1055
  width: 14px;
@@ -1018,8 +1068,9 @@ a.brand:hover { opacity: 0.75; }
1018
1068
  border-left: 3px solid var(--line-strong);
1019
1069
  font-size: 12.5px;
1020
1070
  }
1021
- .reply-panel { display: flex; flex-direction: column; gap: 9px; }
1022
- .reply-role { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--good); display: block; margin-bottom: 2px; }
1071
+ .reply-panel { display: flex; flex-direction: column; gap: 9px; border-left-color: color-mix(in oklch, var(--blue), var(--line) 40%); }
1072
+ /* assistant 角色标签用蓝色,对齐 landing code-note-role;绿色留给断言通过。 */
1073
+ .reply-role { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--blue); display: block; margin-bottom: 2px; }
1023
1074
  .reply-text { color: var(--text); white-space: pre-wrap; word-break: break-word; }
1024
1075
  .reply-think { font-size: 12px; color: var(--muted); }
1025
1076
  .reply-think summary { cursor: pointer; user-select: none; }