niceeval 0.1.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 (94) hide show
  1. package/README.md +164 -0
  2. package/README.zh.md +160 -0
  3. package/bin/niceeval.js +11 -0
  4. package/package.json +96 -0
  5. package/src/agents/bub.ts +223 -0
  6. package/src/agents/builtin.ts +6 -0
  7. package/src/agents/claude-code.ts +96 -0
  8. package/src/agents/codex.ts +122 -0
  9. package/src/agents/index.ts +25 -0
  10. package/src/agents/shared.ts +145 -0
  11. package/src/cli.ts +421 -0
  12. package/src/context/context.test.ts +96 -0
  13. package/src/context/context.ts +427 -0
  14. package/src/context/control-flow.ts +27 -0
  15. package/src/context/session.ts +124 -0
  16. package/src/define.ts +112 -0
  17. package/src/expect/index.ts +243 -0
  18. package/src/i18n/en.ts +131 -0
  19. package/src/i18n/index.ts +39 -0
  20. package/src/i18n/zh-CN.ts +132 -0
  21. package/src/index.ts +39 -0
  22. package/src/loaders/index.ts +56 -0
  23. package/src/o11y/cost.ts +57 -0
  24. package/src/o11y/derive.ts +304 -0
  25. package/src/o11y/otlp/canonical.ts +112 -0
  26. package/src/o11y/otlp/mappers/bub.ts +30 -0
  27. package/src/o11y/otlp/mappers/codex.ts +38 -0
  28. package/src/o11y/otlp/mappers/index.ts +25 -0
  29. package/src/o11y/otlp/parse.ts +330 -0
  30. package/src/o11y/otlp/receiver.ts +100 -0
  31. package/src/o11y/otlp/sandbox-receiver.ts +113 -0
  32. package/src/o11y/otlp/select.ts +82 -0
  33. package/src/o11y/parsers/bub.ts +240 -0
  34. package/src/o11y/parsers/claude-code.ts +270 -0
  35. package/src/o11y/parsers/codex.ts +480 -0
  36. package/src/o11y/parsers/index.ts +37 -0
  37. package/src/o11y/prices.json +9873 -0
  38. package/src/runner/discover.ts +77 -0
  39. package/src/runner/reporters/artifacts.ts +81 -0
  40. package/src/runner/reporters/console.ts +76 -0
  41. package/src/runner/reporters/index.ts +5 -0
  42. package/src/runner/reporters/json.ts +52 -0
  43. package/src/runner/reporters/live.ts +178 -0
  44. package/src/runner/reporters/table.ts +328 -0
  45. package/src/runner/run.ts +860 -0
  46. package/src/runner/sandbox-prep.ts +91 -0
  47. package/src/sandbox/checkpoint.ts +39 -0
  48. package/src/sandbox/docker.ts +539 -0
  49. package/src/sandbox/e2b.ts +203 -0
  50. package/src/sandbox/index.ts +25 -0
  51. package/src/sandbox/registry.ts +60 -0
  52. package/src/sandbox/resolve.ts +131 -0
  53. package/src/sandbox/source-files.ts +30 -0
  54. package/src/sandbox/vercel.ts +236 -0
  55. package/src/scoring/collector.ts +113 -0
  56. package/src/scoring/judge.ts +236 -0
  57. package/src/scoring/scoped.ts +289 -0
  58. package/src/scoring/verdict.ts +20 -0
  59. package/src/source-loc.ts +53 -0
  60. package/src/types.ts +913 -0
  61. package/src/util.test.ts +31 -0
  62. package/src/util.ts +50 -0
  63. package/src/view/app/App.tsx +189 -0
  64. package/src/view/app/components/AttemptModal.tsx +66 -0
  65. package/src/view/app/components/CodeView.tsx +272 -0
  66. package/src/view/app/components/CopyControls.tsx +89 -0
  67. package/src/view/app/components/ExperimentTable.tsx +266 -0
  68. package/src/view/app/components/GroupSelector.tsx +61 -0
  69. package/src/view/app/components/LazyArtifact.tsx +50 -0
  70. package/src/view/app/components/Trace.tsx +100 -0
  71. package/src/view/app/components/Transcript.tsx +130 -0
  72. package/src/view/app/components/primitives.tsx +43 -0
  73. package/src/view/app/components/ui/badge.tsx +21 -0
  74. package/src/view/app/components/ui/dialog.tsx +34 -0
  75. package/src/view/app/components/ui/tabs.tsx +30 -0
  76. package/src/view/app/i18n.ts +341 -0
  77. package/src/view/app/index.html +13 -0
  78. package/src/view/app/lib/cn.ts +7 -0
  79. package/src/view/app/lib/format.ts +73 -0
  80. package/src/view/app/lib/guards.ts +61 -0
  81. package/src/view/app/lib/outcome.ts +96 -0
  82. package/src/view/app/lib/rows.ts +63 -0
  83. package/src/view/app/lib/transcript-data.tsx +121 -0
  84. package/src/view/app/main.tsx +17 -0
  85. package/src/view/app/pages/RunsPage.tsx +83 -0
  86. package/src/view/app/pages/TracesPage.tsx +40 -0
  87. package/src/view/app/shared.ts +10 -0
  88. package/src/view/app/types.ts +114 -0
  89. package/src/view/app/vite.config.ts +26 -0
  90. package/src/view/client-dist/app.css +2 -0
  91. package/src/view/client-dist/app.js +56 -0
  92. package/src/view/index.ts +406 -0
  93. package/src/view/styles.css +1074 -0
  94. package/src/view/template.html +15 -0
@@ -0,0 +1,328 @@
1
+ import type { EvalResult, RunSummary, Usage } from "../../types.ts";
2
+ import { t } from "../../i18n/index.ts";
3
+
4
+ const OUTCOME_ORDER: Record<string, number> = {
5
+ failed: 0,
6
+ errored: 1,
7
+ skipped: 2,
8
+ passed: 3,
9
+ };
10
+
11
+ const OUTCOME_SYM: Record<string, string> = {
12
+ passed: "✓",
13
+ failed: "✗",
14
+ errored: "!",
15
+ skipped: "○",
16
+ };
17
+
18
+ interface ExperimentRow {
19
+ key: string;
20
+ label: string;
21
+ model: string;
22
+ agent: string;
23
+ avgDurationMs: number;
24
+ passRate: number;
25
+ tokens: number;
26
+ cost?: number;
27
+ evals: number;
28
+ passed: number;
29
+ failed: number;
30
+ errored: number;
31
+ skipped: number;
32
+ attempts: number;
33
+ results: EvalResult[];
34
+ }
35
+
36
+ interface EvalRow {
37
+ id: string;
38
+ outcome: EvalResult["outcome"];
39
+ reason: string;
40
+ durationMs: number;
41
+ tokens: number;
42
+ cost?: number;
43
+ passedAttempts: number;
44
+ attempts: number;
45
+ }
46
+
47
+ export function renderRunReport(summary: RunSummary): string {
48
+ const rows = aggregateExperimentRows(summary.results);
49
+ const lines: string[] = ["", t("report.table.experimentsTitle")];
50
+ lines.push(renderTable(
51
+ [
52
+ t("report.table.experiment"),
53
+ t("report.table.model"),
54
+ t("report.table.agent"),
55
+ t("report.table.avgDuration"),
56
+ t("report.table.successRate"),
57
+ t("report.table.tokens"),
58
+ t("report.table.cost"),
59
+ t("report.table.result"),
60
+ ],
61
+ rows.map((row) => [
62
+ row.label,
63
+ row.model,
64
+ row.agent,
65
+ formatDuration(row.avgDurationMs),
66
+ formatPercent(row.passRate),
67
+ formatTokens(row.tokens),
68
+ formatCost(row.cost),
69
+ formatResult(row),
70
+ ]),
71
+ ));
72
+
73
+ for (const row of rows) {
74
+ lines.push("", `${t("report.table.evalTitle")} ${row.label}`);
75
+ lines.push(renderTable(
76
+ [
77
+ t("report.table.status"),
78
+ t("report.table.eval"),
79
+ t("report.table.reason"),
80
+ t("report.table.duration"),
81
+ t("report.table.tokens"),
82
+ t("report.table.cost"),
83
+ t("report.table.runs"),
84
+ ],
85
+ aggregateEvalRows(row.results).map((evalRow) => [
86
+ formatOutcome(evalRow.outcome),
87
+ evalRow.id,
88
+ evalRow.reason,
89
+ formatDuration(evalRow.durationMs),
90
+ formatTokens(evalRow.tokens),
91
+ formatCost(evalRow.cost),
92
+ `${evalRow.passedAttempts}/${evalRow.attempts}`,
93
+ ]),
94
+ { maxWidth: 120, flexibleColumn: 2 },
95
+ ));
96
+ }
97
+
98
+ const tok = (summary.usage?.inputTokens ?? 0) + (summary.usage?.outputTokens ?? 0);
99
+ const tokStr = tok > 0 ? `${formatTokens(tok)} tok` : "— tok";
100
+ const cost = summary.estimatedCostUSD !== undefined ? ` · ${formatCost(summary.estimatedCostUSD)}` : "";
101
+ const parts = [
102
+ t("report.summary.passed", { count: summary.passed }),
103
+ t("report.summary.failed", { count: summary.failed }),
104
+ ...(summary.errored > 0 ? [t("report.summary.errored", { count: summary.errored })] : []),
105
+ t("report.summary.skipped", { count: summary.skipped }),
106
+ ];
107
+ lines.push(t("report.result", {
108
+ parts: parts.join(", "),
109
+ duration: formatDuration(summary.durationMs),
110
+ tokens: tokStr,
111
+ cost,
112
+ }).trimEnd());
113
+
114
+ return `${lines.join("\n")}\n\n`;
115
+ }
116
+
117
+ export function formatTokens(n: number): string {
118
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
119
+ return String(n);
120
+ }
121
+
122
+ export function formatDuration(ms: number): string {
123
+ if (!Number.isFinite(ms) || ms <= 0) return "0ms";
124
+ if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
125
+ if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
126
+ return `${Math.round(ms)}ms`;
127
+ }
128
+
129
+ export function formatCost(n: number | undefined): string {
130
+ if (n === undefined || n <= 0) return "$0";
131
+ return `$${n.toFixed(n < 1 ? 3 : 2)}`;
132
+ }
133
+
134
+ export function formatOutcome(outcome: string): string {
135
+ const sym = OUTCOME_SYM[outcome] ?? "?";
136
+ switch (outcome) {
137
+ case "passed": return `${sym} ${t("report.passed")}`;
138
+ case "failed": return `${sym} ${t("report.failed")}`;
139
+ case "errored": return `${sym} ${t("report.errored")}`;
140
+ case "skipped": return `${sym} ${t("report.skipped")}`;
141
+ default: return `${sym} ${outcome}`;
142
+ }
143
+ }
144
+
145
+ function aggregateExperimentRows(results: EvalResult[]): ExperimentRow[] {
146
+ const groups = new Map<string, EvalResult[]>();
147
+ for (const result of results) {
148
+ const key = result.experimentId ? `exp|||${result.experimentId}` : `legacy|||${result.agent}|||${result.model ?? ""}`;
149
+ groups.set(key, [...(groups.get(key) ?? []), result]);
150
+ }
151
+
152
+ return [...groups.entries()]
153
+ .map(([key, groupResults]) => {
154
+ const first = groupResults[0]!;
155
+ const stats = evalLevelStats(groupResults);
156
+ return {
157
+ key,
158
+ label: displayExperimentName(first.experimentId) ?? fallbackExperimentLabel(first),
159
+ model: first.model ?? t("report.table.default"),
160
+ agent: first.agent,
161
+ avgDurationMs: avg(groupResults.map((r) => r.durationMs)),
162
+ passRate: stats.passRate,
163
+ tokens: totalTokens(groupResults.map((r) => r.usage)),
164
+ cost: sumMaybe(groupResults.map((r) => r.estimatedCostUSD)),
165
+ evals: stats.evals,
166
+ passed: stats.passed,
167
+ failed: stats.failed,
168
+ errored: stats.errored,
169
+ skipped: stats.skipped,
170
+ attempts: groupResults.length,
171
+ results: groupResults.slice().sort((a, b) => a.id.localeCompare(b.id) || a.attempt - b.attempt),
172
+ };
173
+ })
174
+ .sort((a, b) => b.passRate - a.passRate || a.label.localeCompare(b.label));
175
+ }
176
+
177
+ function aggregateEvalRows(results: EvalResult[]): EvalRow[] {
178
+ const byEval = new Map<string, EvalResult[]>();
179
+ for (const result of results) byEval.set(result.id, [...(byEval.get(result.id) ?? []), result]);
180
+ return [...byEval.entries()]
181
+ .map(([id, evalResults]) => {
182
+ const outcome = foldEvalOutcome(evalResults);
183
+ const representative =
184
+ evalResults.find((r) => r.outcome === outcome) ??
185
+ evalResults.find((r) => r.outcome !== "passed") ??
186
+ evalResults[0]!;
187
+ return {
188
+ id,
189
+ outcome,
190
+ reason: outcome === "passed" ? t("report.passed") : reasonFor(representative),
191
+ durationMs: avg(evalResults.map((r) => r.durationMs)),
192
+ tokens: totalTokens(evalResults.map((r) => r.usage)),
193
+ cost: sumMaybe(evalResults.map((r) => r.estimatedCostUSD)),
194
+ passedAttempts: evalResults.filter((r) => r.outcome === "passed").length,
195
+ attempts: evalResults.length,
196
+ };
197
+ })
198
+ .sort((a, b) => OUTCOME_ORDER[a.outcome] - OUTCOME_ORDER[b.outcome] || a.id.localeCompare(b.id));
199
+ }
200
+
201
+ function evalLevelStats(results: EvalResult[]) {
202
+ const byEval = new Map<string, EvalResult[]>();
203
+ for (const r of results) byEval.set(r.id, [...(byEval.get(r.id) ?? []), r]);
204
+ const counts = { passed: 0, failed: 0, errored: 0, skipped: 0 };
205
+ for (const group of byEval.values()) counts[foldEvalOutcome(group)] += 1;
206
+ const ran = counts.passed + counts.failed + counts.errored;
207
+ return { evals: byEval.size, ...counts, passRate: ran ? counts.passed / ran : 0 };
208
+ }
209
+
210
+ function foldEvalOutcome(results: EvalResult[]): EvalResult["outcome"] {
211
+ const outcomes = results.map((r) => r.outcome);
212
+ if (outcomes.some((o) => o === "passed")) return "passed";
213
+ if (outcomes.some((o) => o === "failed")) return "failed";
214
+ if (outcomes.some((o) => o === "errored")) return "errored";
215
+ return "skipped";
216
+ }
217
+
218
+ function reasonFor(result: EvalResult): string {
219
+ if (result.skipReason) return result.skipReason;
220
+ if (result.error) return truncateOneLine(result.error, 120);
221
+ const failed = result.assertions.filter((a) => !a.passed && a.severity === "gate");
222
+ const assertions = failed.length ? failed : result.assertions.slice(0, 2);
223
+ if (!assertions.length) return "";
224
+ return truncateOneLine(
225
+ assertions
226
+ .map((a) => {
227
+ const threshold = a.threshold !== undefined ? ` ${t("report.assertionThreshold", { score: a.score.toFixed(2), threshold: a.threshold }).trim()}` : "";
228
+ return `${a.severity}: ${a.name}${threshold}`;
229
+ })
230
+ .join(" · "),
231
+ 120,
232
+ );
233
+ }
234
+
235
+ function formatResult(row: ExperimentRow): string {
236
+ const failures = row.failed + row.errored;
237
+ const suffix = row.skipped > 0 ? ` · ${row.skipped} ${t("report.skipped")}` : "";
238
+ return `${row.passed}/${row.evals} ${t("report.passed")} · ${failures} ${t("report.failed")}${suffix}`;
239
+ }
240
+
241
+ function displayExperimentName(id: string | undefined): string | undefined {
242
+ if (!id) return undefined;
243
+ return id.split("/").filter(Boolean).at(-1) ?? id;
244
+ }
245
+
246
+ function fallbackExperimentLabel(result: EvalResult): string {
247
+ if (result.experiment?.id) return displayExperimentName(result.experiment.id) ?? result.experiment.id;
248
+ if (result.model) return `${result.agent}/${result.model}`;
249
+ return result.agent || "ad hoc run";
250
+ }
251
+
252
+ function totalTokens(items: Array<Usage | undefined>): number {
253
+ return items.reduce((n, u) => n + (u?.inputTokens ?? 0) + (u?.outputTokens ?? 0), 0);
254
+ }
255
+
256
+ function sumMaybe(items: Array<number | undefined>): number | undefined {
257
+ const known = items.filter((n): n is number => n !== undefined);
258
+ return known.length ? known.reduce((sum, n) => sum + n, 0) : undefined;
259
+ }
260
+
261
+ function avg(items: number[]): number {
262
+ return items.length ? items.reduce((sum, n) => sum + n, 0) / items.length : 0;
263
+ }
264
+
265
+ function formatPercent(v: number): string {
266
+ return `${Math.round(v * 100)}%`;
267
+ }
268
+
269
+ function renderTable(headers: string[], rows: string[][], opts: { maxWidth?: number; flexibleColumn?: number } = {}): string {
270
+ const rawRows = [headers, ...rows];
271
+ const widths = headers.map((_, col) => Math.max(...rawRows.map((row) => displayWidth(row[col] ?? ""))));
272
+ const maxWidth = opts.maxWidth ?? 140;
273
+ const tableWidth = widths.reduce((sum, w) => sum + w, 0) + (headers.length - 1) * 3;
274
+ if (tableWidth > maxWidth && opts.flexibleColumn !== undefined) {
275
+ const over = tableWidth - maxWidth;
276
+ widths[opts.flexibleColumn] = Math.max(18, widths[opts.flexibleColumn]! - over);
277
+ }
278
+
279
+ const lines = [formatRow(headers, widths), widths.map((w) => "-".repeat(w)).join("-+-")];
280
+ for (const row of rows) lines.push(formatRow(row, widths));
281
+ return lines.join("\n");
282
+ }
283
+
284
+ function formatRow(row: string[], widths: number[]): string {
285
+ return row.map((cell, i) => padEnd(truncateToWidth(cell ?? "", widths[i]!), widths[i]!)).join(" | ");
286
+ }
287
+
288
+ function truncateOneLine(s: string, width: number): string {
289
+ return truncateToWidth(s.replace(/\s+/g, " ").trim(), width);
290
+ }
291
+
292
+ function truncateToWidth(s: string, width: number): string {
293
+ if (displayWidth(s) <= width) return s;
294
+ const ellipsis = "…";
295
+ let out = "";
296
+ for (const char of s) {
297
+ if (displayWidth(out + char + ellipsis) > width) break;
298
+ out += char;
299
+ }
300
+ return out + ellipsis;
301
+ }
302
+
303
+ function padEnd(s: string, width: number): string {
304
+ return s + " ".repeat(Math.max(0, width - displayWidth(s)));
305
+ }
306
+
307
+ function displayWidth(s: string): number {
308
+ let width = 0;
309
+ for (const char of s) {
310
+ const code = char.codePointAt(0) ?? 0;
311
+ width += isWide(code) ? 2 : 1;
312
+ }
313
+ return width;
314
+ }
315
+
316
+ function isWide(code: number): boolean {
317
+ return (
318
+ (code >= 0x1100 && code <= 0x115f) ||
319
+ (code >= 0x2e80 && code <= 0xa4cf) ||
320
+ (code >= 0xac00 && code <= 0xd7a3) ||
321
+ (code >= 0xf900 && code <= 0xfaff) ||
322
+ (code >= 0xfe10 && code <= 0xfe19) ||
323
+ (code >= 0xfe30 && code <= 0xfe6f) ||
324
+ (code >= 0xff00 && code <= 0xff60) ||
325
+ (code >= 0xffe0 && code <= 0xffe6) ||
326
+ (code >= 0x1f300 && code <= 0x1faff)
327
+ );
328
+ }