@garuda-agent/garuda 0.18.1 → 0.18.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.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/{acpCommand-JAYNMIMP.js → acpCommand-PUJHFWYU.js} +11 -12
- package/dist/advise-C777SZ5M.js +433 -0
- package/dist/buildInfo-Z4P7QAQL.js +8 -0
- package/dist/{chunk-QLBCM2YQ.js → chunk-2FIV2VZP.js} +2 -2
- package/dist/{chunk-TLA2PDGM.js → chunk-4BELTWP4.js} +5 -5
- package/dist/{chunk-JMH5W5DG.js → chunk-65NUEOI7.js} +44 -1
- package/dist/chunk-6JQTPW3P.js +88 -0
- package/dist/chunk-A5DLAXB4.js +1780 -0
- package/dist/{chunk-DQSCCE77.js → chunk-AX3A5KPH.js} +5 -5
- package/dist/chunk-CFDRP6XW.js +9 -0
- package/dist/chunk-E6YRCDSF.js +9 -0
- package/dist/{chunk-34WBDQBL.js → chunk-HP6EGFHV.js} +18 -18
- package/dist/{chunk-GKU62MHQ.js → chunk-LWNMEGDF.js} +2 -2
- package/dist/{chunk-5UDYQ5MK.js → chunk-STVFWSIL.js} +5 -7
- package/dist/{chunk-IKPGHM54.js → chunk-YEH2OONM.js} +3 -3
- package/dist/cli/index.js +39 -40
- package/dist/{evalCommand-BLJJ4G76.js → evalCommand-MRO6ENCA.js} +70 -42
- package/dist/{evalTune-W53KAD4V.js → evalTune-VL5RHYOG.js} +17 -18
- package/dist/{export-E677HIQS.js → export-ZPF3AMO6.js} +5 -5
- package/dist/{exportHtml-BMV73YRO.js → exportHtml-ISISG5HG.js} +4 -4
- package/dist/{inkChat-EVLYDC3K.js → inkChat-UDFX6GH6.js} +11 -12
- package/dist/{manager-L25IUXHW.js → manager-YH7722C2.js} +2 -2
- package/dist/runner-EBF6EOYM.js +97 -0
- package/dist/{setupCommand-OSOMFHDQ.js → setupCommand-YMF6I3MD.js} +8 -9
- package/dist/{tune-VX2WQTHS.js → tune-KSNKBPNJ.js} +8 -9
- package/package.json +1 -1
- package/dist/advise-HDKJGNBL.js +0 -195
- package/dist/buildInfo-K73FFI5S.js +0 -8
- package/dist/chunk-CWVXOSAA.js +0 -894
- package/dist/chunk-DUYLVGPY.js +0 -821
- package/dist/chunk-H7LIBGZT.js +0 -9
- package/dist/chunk-IYGJWX65.js +0 -9
- package/dist/chunk-NH72AELP.js +0 -48
package/dist/chunk-CWVXOSAA.js
DELETED
|
@@ -1,894 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
LINK_CANDIDATES,
|
|
4
|
-
cleanArgs
|
|
5
|
-
} from "./chunk-WK5NE6RY.js";
|
|
6
|
-
import {
|
|
7
|
-
MANIFEST,
|
|
8
|
-
TEST_PATH,
|
|
9
|
-
tail
|
|
10
|
-
} from "./chunk-AOGFYUKG.js";
|
|
11
|
-
import {
|
|
12
|
-
git,
|
|
13
|
-
shellWord
|
|
14
|
-
} from "./chunk-QXTHEFKP.js";
|
|
15
|
-
import {
|
|
16
|
-
DEFAULT_ENV_ALLOWLIST,
|
|
17
|
-
HostExecutor,
|
|
18
|
-
denyAround
|
|
19
|
-
} from "./chunk-LMKGSYJC.js";
|
|
20
|
-
|
|
21
|
-
// src/evals/stats.ts
|
|
22
|
-
var BOOTSTRAP_DRAWS = 1e4;
|
|
23
|
-
var DEFAULT_SEED = 1;
|
|
24
|
-
function seeded(seed) {
|
|
25
|
-
let a = seed >>> 0;
|
|
26
|
-
return () => {
|
|
27
|
-
a = a + 1831565813 >>> 0;
|
|
28
|
-
let t = a;
|
|
29
|
-
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
30
|
-
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
31
|
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function shuffled(items, seed) {
|
|
35
|
-
const out = [...items];
|
|
36
|
-
const random = seeded(seed);
|
|
37
|
-
for (let i = out.length - 1; i > 0; i--) {
|
|
38
|
-
const j = Math.floor(random() * (i + 1));
|
|
39
|
-
[out[i], out[j]] = [out[j], out[i]];
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
function byTask(results) {
|
|
44
|
-
const map = /* @__PURE__ */ new Map();
|
|
45
|
-
for (const r of results) {
|
|
46
|
-
let t = map.get(r.id);
|
|
47
|
-
if (t === void 0) {
|
|
48
|
-
t = { id: r.id, runs: 0, passes: 0, errors: 0, results: [] };
|
|
49
|
-
map.set(r.id, t);
|
|
50
|
-
}
|
|
51
|
-
t.runs++;
|
|
52
|
-
if (r.passed) t.passes++;
|
|
53
|
-
if (r.stopReason === "error") t.errors++;
|
|
54
|
-
t.results.push(r);
|
|
55
|
-
}
|
|
56
|
-
return [...map.values()];
|
|
57
|
-
}
|
|
58
|
-
function passInterval(results, options = {}) {
|
|
59
|
-
const tasks = byTask(results);
|
|
60
|
-
const runs = tasks.reduce((s, t) => s + t.runs, 0);
|
|
61
|
-
if (tasks.length === 0 || runs === 0) return void 0;
|
|
62
|
-
const seed = options.seed ?? DEFAULT_SEED;
|
|
63
|
-
const draws = options.draws ?? BOOTSTRAP_DRAWS;
|
|
64
|
-
const level = options.level ?? 0.95;
|
|
65
|
-
const random = seeded(seed);
|
|
66
|
-
const rates = new Float64Array(draws);
|
|
67
|
-
for (let d = 0; d < draws; d++) {
|
|
68
|
-
let passes = 0;
|
|
69
|
-
let n = 0;
|
|
70
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
71
|
-
const t = tasks[Math.floor(random() * tasks.length)];
|
|
72
|
-
passes += t.passes;
|
|
73
|
-
n += t.runs;
|
|
74
|
-
}
|
|
75
|
-
rates[d] = passes / n;
|
|
76
|
-
}
|
|
77
|
-
rates.sort();
|
|
78
|
-
const tail2 = (1 - level) / 2;
|
|
79
|
-
return {
|
|
80
|
-
rate: tasks.reduce((s, t) => s + t.passes, 0) / runs,
|
|
81
|
-
low: quantile(rates, tail2),
|
|
82
|
-
high: quantile(rates, 1 - tail2),
|
|
83
|
-
level,
|
|
84
|
-
draws,
|
|
85
|
-
seed,
|
|
86
|
-
tasks: tasks.length
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function consistency(results) {
|
|
90
|
-
const repeated = byTask(results).filter((t) => t.runs >= 2);
|
|
91
|
-
return {
|
|
92
|
-
repeated: repeated.length,
|
|
93
|
-
allPassed: repeated.filter((t) => t.passes === t.runs).map((t) => t.id),
|
|
94
|
-
nonePassed: repeated.filter((t) => t.passes === 0 && t.errors < t.runs).map((t) => t.id),
|
|
95
|
-
onlyErrors: repeated.filter((t) => t.errors === t.runs).map((t) => t.id)
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
function spread(results) {
|
|
99
|
-
const scored = results.filter((r) => r.stopReason !== "error");
|
|
100
|
-
if (scored.length === 0) return void 0;
|
|
101
|
-
const of = (f) => {
|
|
102
|
-
const sorted = Float64Array.from(scored.map(f)).sort();
|
|
103
|
-
return { median: quantile(sorted, 0.5), p95: quantile(sorted, 0.95) };
|
|
104
|
-
};
|
|
105
|
-
return {
|
|
106
|
-
runs: scored.length,
|
|
107
|
-
steps: of((r) => r.steps),
|
|
108
|
-
tokens: of((r) => r.tokens),
|
|
109
|
-
durationMs: of((r) => r.durationMs)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function stepsSd(runs) {
|
|
113
|
-
const steps = runs.filter((r) => r.stopReason !== "error").map((r) => r.steps);
|
|
114
|
-
if (steps.length < 2) return void 0;
|
|
115
|
-
const mean = steps.reduce((s, x) => s + x, 0) / steps.length;
|
|
116
|
-
return Math.sqrt(steps.reduce((s, x) => s + (x - mean) ** 2, 0) / (steps.length - 1));
|
|
117
|
-
}
|
|
118
|
-
function costPerPass(results) {
|
|
119
|
-
if (results.some((r) => r.costUsd === void 0)) return { kind: "unknown" };
|
|
120
|
-
const totalUsd = results.reduce((s, r) => s + (r.costUsd ?? 0), 0);
|
|
121
|
-
const passes = results.filter((r) => r.passed).length;
|
|
122
|
-
if (passes === 0) return { kind: "none", totalUsd };
|
|
123
|
-
return {
|
|
124
|
-
kind: "value",
|
|
125
|
-
usd: totalUsd / passes,
|
|
126
|
-
minimum: results.some((r) => r.costIncomplete === true),
|
|
127
|
-
totalUsd,
|
|
128
|
-
passes
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function unexpectedChanges(results) {
|
|
132
|
-
const measured = results.filter((r) => r.unexpected !== void 0);
|
|
133
|
-
return {
|
|
134
|
-
runs: measured.filter((r) => (r.unexpected?.length ?? 0) > 0).length,
|
|
135
|
-
files: measured.reduce((s, r) => s + (r.unexpected?.length ?? 0), 0),
|
|
136
|
-
measured: measured.length
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
function quantile(sorted, q) {
|
|
140
|
-
if (sorted.length === 0) return Number.NaN;
|
|
141
|
-
const pos = (sorted.length - 1) * q;
|
|
142
|
-
const lo = Math.floor(pos);
|
|
143
|
-
const hi = Math.ceil(pos);
|
|
144
|
-
const a = sorted[lo];
|
|
145
|
-
const b = sorted[hi];
|
|
146
|
-
return a + (b - a) * (pos - lo);
|
|
147
|
-
}
|
|
148
|
-
function mcpUse(results) {
|
|
149
|
-
const measured = results.filter((r) => r.mcpCalls !== void 0);
|
|
150
|
-
if (measured.length === 0) return void 0;
|
|
151
|
-
return {
|
|
152
|
-
calls: measured.reduce((sum, r) => sum + (r.mcpCalls ?? 0), 0),
|
|
153
|
-
runs: measured.filter((r) => (r.mcpCalls ?? 0) > 0).length,
|
|
154
|
-
measured: measured.length
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function mcpUseText(use) {
|
|
158
|
-
return `${use.calls} in ${use.runs} of ${use.measured} runs${use.runs === 0 ? " (the agent never used the server)" : ""}`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// src/evals/compare.ts
|
|
162
|
-
import { readFile, stat } from "fs/promises";
|
|
163
|
-
import { join } from "path";
|
|
164
|
-
async function loadReport(path) {
|
|
165
|
-
const file = (await stat(path)).isDirectory() ? join(path, "report.json") : path;
|
|
166
|
-
const data = JSON.parse(await readFile(file, "utf8"));
|
|
167
|
-
return parseReport(data, file);
|
|
168
|
-
}
|
|
169
|
-
function parseReport(data, path) {
|
|
170
|
-
if (!Array.isArray(data.results)) throw new Error(`${path} is not an eval report (no results).`);
|
|
171
|
-
const schema = typeof data.schema === "number" ? data.schema : 1;
|
|
172
|
-
const tasks = schema >= 2 && data.tasks !== null && typeof data.tasks === "object" ? data.tasks : void 0;
|
|
173
|
-
return {
|
|
174
|
-
path,
|
|
175
|
-
schema,
|
|
176
|
-
model: typeof data.spec === "string" ? data.spec : typeof data.model === "string" ? data.model : void 0,
|
|
177
|
-
seed: typeof data.seed === "number" ? data.seed : void 0,
|
|
178
|
-
tasks,
|
|
179
|
-
results: data.results,
|
|
180
|
-
mcp: mcpServerOf(data.mcp)
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function mcpServerOf(value) {
|
|
184
|
-
if (value === null || typeof value !== "object") return void 0;
|
|
185
|
-
const server = value.server;
|
|
186
|
-
return typeof server === "string" ? server : void 0;
|
|
187
|
-
}
|
|
188
|
-
function compareReports(a, b, options = {}) {
|
|
189
|
-
const problems = [];
|
|
190
|
-
const notes = [];
|
|
191
|
-
const idsA = [...new Set(a.results.map((r) => r.id))].sort();
|
|
192
|
-
const idsB = [...new Set(b.results.map((r) => r.id))].sort();
|
|
193
|
-
if (idsA.join("\n") !== idsB.join("\n")) {
|
|
194
|
-
const onlyA = idsA.filter((id) => !idsB.includes(id));
|
|
195
|
-
const onlyB = idsB.filter((id) => !idsA.includes(id));
|
|
196
|
-
problems.push(
|
|
197
|
-
`The reports ran different tasks${onlyA.length > 0 ? `; only in A: ${onlyA.join(", ")}` : ""}${onlyB.length > 0 ? `; only in B: ${onlyB.join(", ")}` : ""}.`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (a.tasks !== void 0 && b.tasks !== void 0) {
|
|
201
|
-
const changed = idsA.filter((id) => idsB.includes(id) && a.tasks?.[id] !== b.tasks?.[id]);
|
|
202
|
-
if (changed.length > 0) {
|
|
203
|
-
problems.push(
|
|
204
|
-
`These tasks changed between the runs (their hashes differ): ${changed.join(", ")}. Run both arms on the same tasks.`
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
const old = [a, b].filter((r) => r.tasks === void 0).map((r) => r.path);
|
|
209
|
-
notes.push(
|
|
210
|
-
`${old.join(" and ")} ${old.length === 1 ? "has" : "have"} no task hashes (a report before 0.17): Garuda cannot check that the tasks were the same.`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
if (problems.length > 0) return { problems };
|
|
214
|
-
if (idsA.length === 0) return { problems: ["The reports have no runs."] };
|
|
215
|
-
const tasksA = new Map(byTask(a.results).map((t) => [t.id, t]));
|
|
216
|
-
const tasksB = new Map(byTask(b.results).map((t) => [t.id, t]));
|
|
217
|
-
const rows = idsA.map((id) => {
|
|
218
|
-
const ta = tasksA.get(id);
|
|
219
|
-
const tb = tasksB.get(id);
|
|
220
|
-
const arm = (t) => {
|
|
221
|
-
const results = t?.results ?? [];
|
|
222
|
-
return {
|
|
223
|
-
passes: t?.passes ?? 0,
|
|
224
|
-
runs: t?.runs ?? 0,
|
|
225
|
-
costUsd: results.some((r) => r.costUsd === void 0) ? void 0 : results.reduce((s, r) => s + (r.costUsd ?? 0), 0),
|
|
226
|
-
minimum: results.some((r) => r.costIncomplete === true)
|
|
227
|
-
};
|
|
228
|
-
};
|
|
229
|
-
return { id, a: arm(ta), b: arm(tb) };
|
|
230
|
-
});
|
|
231
|
-
const rate = (side, list) => list.reduce((s, r) => s + r[side].passes, 0) / list.reduce((s, r) => s + r[side].runs, 0);
|
|
232
|
-
const rateA = rate("a", rows);
|
|
233
|
-
const rateB = rate("b", rows);
|
|
234
|
-
const seed = options.seed ?? DEFAULT_SEED;
|
|
235
|
-
const draws = options.draws ?? BOOTSTRAP_DRAWS;
|
|
236
|
-
const level = options.level ?? 0.95;
|
|
237
|
-
const random = seeded(seed);
|
|
238
|
-
const diffs = new Float64Array(draws);
|
|
239
|
-
for (let d = 0; d < draws; d++) {
|
|
240
|
-
let pa = 0;
|
|
241
|
-
let na = 0;
|
|
242
|
-
let pb = 0;
|
|
243
|
-
let nb = 0;
|
|
244
|
-
for (let i = 0; i < rows.length; i++) {
|
|
245
|
-
const r = rows[Math.floor(random() * rows.length)];
|
|
246
|
-
pa += r.a.passes;
|
|
247
|
-
na += r.a.runs;
|
|
248
|
-
pb += r.b.passes;
|
|
249
|
-
nb += r.b.runs;
|
|
250
|
-
}
|
|
251
|
-
diffs[d] = pb / nb - pa / na;
|
|
252
|
-
}
|
|
253
|
-
diffs.sort();
|
|
254
|
-
const tail2 = (1 - level) / 2;
|
|
255
|
-
const low = quantile(diffs, tail2);
|
|
256
|
-
const high = quantile(diffs, 1 - tail2);
|
|
257
|
-
const verdict = low > 0 ? "B passes more" : high < 0 ? "B passes less" : "no clear difference";
|
|
258
|
-
if (low === high) {
|
|
259
|
-
notes.push(
|
|
260
|
-
"Every draw gave the same pass-rate difference, so the interval shows no uncertainty: more tasks, or tasks that sometimes fail, are needed to measure it."
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (rows.length < 10) notes.push(roughNote(rows.length));
|
|
264
|
-
if (a.seed !== void 0 && b.seed !== void 0 && a.seed !== b.seed) {
|
|
265
|
-
notes.push(
|
|
266
|
-
`The arms ran with different seeds (${a.seed} and ${b.seed}): their run orders differ.`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (a.mcp !== b.mcp) {
|
|
270
|
-
notes.push(
|
|
271
|
-
`The arms differ in the MCP server: A ${a.mcp ?? "none"}, B ${b.mcp ?? "none"}. For an MCP A/B, keep every other option the same.`
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
const cost = costRatio(rows, seed, draws, level);
|
|
275
|
-
if (typeof cost === "string") notes.push(cost);
|
|
276
|
-
else if (cost.skipped > draws * 0.05) {
|
|
277
|
-
notes.push(
|
|
278
|
-
`The cost interval skipped ${cost.skipped} of ${draws} draws (an arm had no pass in them).`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
if (rows.some((r) => r.a.minimum || r.b.minimum)) {
|
|
282
|
-
notes.push("A run's cost is a minimum (no reported usage): the cost ratio is approximate.");
|
|
283
|
-
}
|
|
284
|
-
const runsA = rows.map((r) => r.a.runs);
|
|
285
|
-
const runsB = rows.map((r) => r.b.runs);
|
|
286
|
-
if (Math.min(...runsA, ...runsB) < 3) {
|
|
287
|
-
notes.push(
|
|
288
|
-
"Some tasks have fewer than 3 runs in an arm: repeat each task 3 times or more (--repeat 3)."
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
comparison: {
|
|
293
|
-
rows,
|
|
294
|
-
rateA,
|
|
295
|
-
rateB,
|
|
296
|
-
difference: { value: rateB - rateA, low, high, level, draws, seed },
|
|
297
|
-
verdict,
|
|
298
|
-
...typeof cost === "string" ? {} : { costRatio: cost },
|
|
299
|
-
notes
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
function costRatio(rows, seed, draws, level) {
|
|
304
|
-
if (rows.some((r) => r.a.costUsd === void 0 || r.b.costUsd === void 0)) {
|
|
305
|
-
return "A run's cost is unknown: no cost comparison.";
|
|
306
|
-
}
|
|
307
|
-
const per = (list, side) => {
|
|
308
|
-
const passes = list.reduce((s, r) => s + r[side].passes, 0);
|
|
309
|
-
const cost = list.reduce((s, r) => s + (r[side].costUsd ?? 0), 0);
|
|
310
|
-
return passes === 0 ? void 0 : cost / passes;
|
|
311
|
-
};
|
|
312
|
-
const a = per(rows, "a");
|
|
313
|
-
const b = per(rows, "b");
|
|
314
|
-
if (a === void 0 || b === void 0 || a === 0) {
|
|
315
|
-
return "An arm has no pass (or no cost): no cost ratio.";
|
|
316
|
-
}
|
|
317
|
-
const random = seeded(seed);
|
|
318
|
-
const ratios = [];
|
|
319
|
-
const drawn = new Array(rows.length);
|
|
320
|
-
for (let d = 0; d < draws; d++) {
|
|
321
|
-
for (let i = 0; i < rows.length; i++) {
|
|
322
|
-
drawn[i] = rows[Math.floor(random() * rows.length)];
|
|
323
|
-
}
|
|
324
|
-
const ca = per(drawn, "a");
|
|
325
|
-
const cb = per(drawn, "b");
|
|
326
|
-
if (ca !== void 0 && cb !== void 0 && ca > 0) ratios.push(cb / ca);
|
|
327
|
-
}
|
|
328
|
-
const sorted = Float64Array.from(ratios).sort();
|
|
329
|
-
const tail2 = (1 - level) / 2;
|
|
330
|
-
const low = quantile(sorted, tail2);
|
|
331
|
-
const high = quantile(sorted, 1 - tail2);
|
|
332
|
-
return {
|
|
333
|
-
value: b / a,
|
|
334
|
-
low,
|
|
335
|
-
high,
|
|
336
|
-
draws,
|
|
337
|
-
skipped: draws - ratios.length,
|
|
338
|
-
verdict: low > 1 ? "B costs more per accepted change" : high < 1 ? "B costs less per accepted change" : "no clear difference"
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function roughNote(tasks) {
|
|
342
|
-
return `Only ${tasks} task${tasks === 1 ? "" : "s"}: with fewer than 10 tasks the interval is a rough guide.`;
|
|
343
|
-
}
|
|
344
|
-
var pct = (x) => `${Math.round(x * 100)}%`;
|
|
345
|
-
var signed = (x) => {
|
|
346
|
-
const points = Math.round(Math.abs(x) * 100);
|
|
347
|
-
return points === 0 ? "0 pts" : `${x > 0 ? "+" : "\u2212"}${points} pts`;
|
|
348
|
-
};
|
|
349
|
-
function formatComparison(a, b, c) {
|
|
350
|
-
const width = Math.max(4, ...c.rows.map((r) => r.id.length));
|
|
351
|
-
const lines2 = [
|
|
352
|
-
`A: ${a.path}${a.model === void 0 ? "" : ` (${a.model})`}`,
|
|
353
|
-
`B: ${b.path}${b.model === void 0 ? "" : ` (${b.model})`}`,
|
|
354
|
-
"",
|
|
355
|
-
`${"Task".padEnd(width)} A B`,
|
|
356
|
-
...c.rows.map(
|
|
357
|
-
(r) => `${r.id.padEnd(width)} ${`${r.a.passes}/${r.a.runs}`.padEnd(7)} ${r.b.passes}/${r.b.runs}`
|
|
358
|
-
),
|
|
359
|
-
"",
|
|
360
|
-
`Pass rate over all attempts: A ${pct(c.rateA)}, B ${pct(c.rateB)}; B \u2212 A ${signed(c.difference.value)} (95% paired interval ${signed(c.difference.low)} to ${signed(c.difference.high)}; ${c.difference.draws} draws, seed ${c.difference.seed})`,
|
|
361
|
-
`Verdict: ${c.verdict}${c.verdict === "no clear difference" ? " (the interval contains 0)" : ""}.`
|
|
362
|
-
];
|
|
363
|
-
for (const [name, report] of [
|
|
364
|
-
["A", a],
|
|
365
|
-
["B", b]
|
|
366
|
-
]) {
|
|
367
|
-
const cost = costPerPass(report.results);
|
|
368
|
-
const u = unexpectedChanges(report.results);
|
|
369
|
-
const s = spread(report.results);
|
|
370
|
-
const mcp = mcpUse(report.results);
|
|
371
|
-
lines2.push(
|
|
372
|
-
`${name}: cost per accepted change ${cost.kind === "unknown" ? "unknown" : cost.kind === "none" ? "no passes" : `${cost.minimum ? "\u2265 " : ""}$${cost.usd.toFixed(4)}`} \xB7 unexpected changes ${u.measured === 0 ? "not measured" : `${u.runs} of ${u.measured} runs`} \xB7 median time ${s === void 0 ? "\u2013" : `${Math.round(s.durationMs.median / 1e3)} s`}${mcp === void 0 ? "" : ` \xB7 MCP tool calls ${mcpUseText(mcp)}`}`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
if (c.costRatio !== void 0) {
|
|
376
|
-
const x = (v) => `\xD7${v.toFixed(2)}`;
|
|
377
|
-
lines2.push(
|
|
378
|
-
`Cost per accepted change: B/A ${x(c.costRatio.value)} (95% paired interval ${x(c.costRatio.low)} to ${x(c.costRatio.high)}). Verdict: ${c.costRatio.verdict}${c.costRatio.verdict === "no clear difference" ? " (the interval contains \xD71.00)" : ""}.`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
for (const note of c.notes) lines2.push(`Note: ${note}`);
|
|
382
|
-
return lines2.join("\n");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// src/evals/toolchains.ts
|
|
386
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
387
|
-
import { tmpdir } from "os";
|
|
388
|
-
import { join as join3 } from "path";
|
|
389
|
-
|
|
390
|
-
// src/evals/files.ts
|
|
391
|
-
import { mkdirSync } from "fs";
|
|
392
|
-
import { writeFile } from "fs/promises";
|
|
393
|
-
import { dirname, join as join2 } from "path";
|
|
394
|
-
async function writeFiles(root, files) {
|
|
395
|
-
for (const [path, content] of Object.entries(files)) {
|
|
396
|
-
const full = join2(root, path);
|
|
397
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
398
|
-
await writeFile(full, content);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// src/evals/projects.ts
|
|
403
|
-
var MAVEN_POM = `<?xml version="1.0" encoding="UTF-8"?>
|
|
404
|
-
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
405
|
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
406
|
-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
407
|
-
<modelVersion>4.0.0</modelVersion>
|
|
408
|
-
<groupId>dev.garuda.eval</groupId>
|
|
409
|
-
<artifactId>eval</artifactId>
|
|
410
|
-
<version>1.0.0</version>
|
|
411
|
-
|
|
412
|
-
<properties>
|
|
413
|
-
<maven.compiler.release>17</maven.compiler.release>
|
|
414
|
-
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
415
|
-
</properties>
|
|
416
|
-
|
|
417
|
-
<dependencies>
|
|
418
|
-
<dependency>
|
|
419
|
-
<groupId>org.junit.jupiter</groupId>
|
|
420
|
-
<artifactId>junit-jupiter</artifactId>
|
|
421
|
-
<version>5.11.4</version>
|
|
422
|
-
<scope>test</scope>
|
|
423
|
-
</dependency>
|
|
424
|
-
</dependencies>
|
|
425
|
-
|
|
426
|
-
<build>
|
|
427
|
-
<plugins>
|
|
428
|
-
<plugin>
|
|
429
|
-
<groupId>org.apache.maven.plugins</groupId>
|
|
430
|
-
<artifactId>maven-resources-plugin</artifactId>
|
|
431
|
-
<version>3.3.1</version>
|
|
432
|
-
</plugin>
|
|
433
|
-
<plugin>
|
|
434
|
-
<groupId>org.apache.maven.plugins</groupId>
|
|
435
|
-
<artifactId>maven-compiler-plugin</artifactId>
|
|
436
|
-
<version>3.13.0</version>
|
|
437
|
-
</plugin>
|
|
438
|
-
<plugin>
|
|
439
|
-
<groupId>org.apache.maven.plugins</groupId>
|
|
440
|
-
<artifactId>maven-surefire-plugin</artifactId>
|
|
441
|
-
<version>3.5.2</version>
|
|
442
|
-
</plugin>
|
|
443
|
-
</plugins>
|
|
444
|
-
</build>
|
|
445
|
-
</project>
|
|
446
|
-
`;
|
|
447
|
-
var MAVEN_CHECK = "test ! -e .mvn && mvn -B -q -o test";
|
|
448
|
-
var PYPROJECT = `[project]
|
|
449
|
-
name = "evalpkg"
|
|
450
|
-
version = "0.1.0"
|
|
451
|
-
requires-python = ">=3.9"
|
|
452
|
-
|
|
453
|
-
[tool.pytest.ini_options]
|
|
454
|
-
pythonpath = ["src"]
|
|
455
|
-
testpaths = ["tests"]
|
|
456
|
-
`;
|
|
457
|
-
var PYTEST_CHECK = "python3 -m pytest -q -c pyproject.toml --noconftest -p no:cacheprovider";
|
|
458
|
-
function isProtectedPath(path) {
|
|
459
|
-
return /^(test|tests|src\/test)\//.test(path) || path === "pom.xml" || path === "pyproject.toml";
|
|
460
|
-
}
|
|
461
|
-
var lines = (...l) => `${l.join("\n")}
|
|
462
|
-
`;
|
|
463
|
-
|
|
464
|
-
// src/evals/toolchains.ts
|
|
465
|
-
var TOOLCHAIN_ENV = ["JAVA_HOME", "GRADLE_USER_HOME", "VIRTUAL_ENV"];
|
|
466
|
-
var TOOLCHAINS = {
|
|
467
|
-
maven: {
|
|
468
|
-
id: "maven",
|
|
469
|
-
title: "Maven with JUnit 5 in ~/.m2",
|
|
470
|
-
files: {
|
|
471
|
-
"pom.xml": MAVEN_POM,
|
|
472
|
-
"src/main/java/probe/Probe.java": "package probe;\n\npublic final class Probe {\n public static int one() {\n return 1;\n }\n}\n",
|
|
473
|
-
"src/test/java/probe/ProbeTest.java": "package probe;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\n\nimport org.junit.jupiter.api.Test;\n\nclass ProbeTest {\n @Test\n void one() {\n assertEquals(1, Probe.one());\n }\n}\n"
|
|
474
|
-
},
|
|
475
|
-
probe: "mvn -B -q -o test",
|
|
476
|
-
prepare: "mvn -B -q test",
|
|
477
|
-
hint: "The Java tasks need a JDK (17 or later), Maven, and JUnit 5 in ~/.m2. Run `garuda eval --prepare java` once: it downloads the plugins and JUnit with Maven (network)."
|
|
478
|
-
},
|
|
479
|
-
pytest: {
|
|
480
|
-
id: "pytest",
|
|
481
|
-
title: "Python 3 with pytest",
|
|
482
|
-
files: {
|
|
483
|
-
"pyproject.toml": PYPROJECT,
|
|
484
|
-
"src/probe/__init__.py": "def one():\n return 1\n",
|
|
485
|
-
"tests/test_probe.py": "from probe import one\n\n\ndef test_one():\n assert one() == 1\n"
|
|
486
|
-
},
|
|
487
|
-
probe: "python3 -m pytest -q -p no:cacheprovider",
|
|
488
|
-
hint: "The Python tasks need python3 with pytest 7 or later. Install it, for example with `python3 -m pip install --user pytest`."
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
var PROBE_TIMEOUT_MS = 12e4;
|
|
492
|
-
var PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
493
|
-
async function checkToolchains(ids) {
|
|
494
|
-
return Promise.all(
|
|
495
|
-
[...new Set(ids)].map(async (id) => {
|
|
496
|
-
const toolchain = TOOLCHAINS[id];
|
|
497
|
-
const r = await inScratch(toolchain, toolchain.probe, PROBE_TIMEOUT_MS);
|
|
498
|
-
return { id, ...r };
|
|
499
|
-
})
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
async function prepareToolchain(id) {
|
|
503
|
-
const toolchain = TOOLCHAINS[id];
|
|
504
|
-
if (toolchain.prepare === void 0) return { ok: false, output: toolchain.hint };
|
|
505
|
-
return inScratch(toolchain, toolchain.prepare, PREPARE_TIMEOUT_MS);
|
|
506
|
-
}
|
|
507
|
-
function toolchainId(name) {
|
|
508
|
-
const aliases = {
|
|
509
|
-
java: "maven",
|
|
510
|
-
maven: "maven",
|
|
511
|
-
python: "pytest",
|
|
512
|
-
pytest: "pytest"
|
|
513
|
-
};
|
|
514
|
-
return aliases[name.toLowerCase()];
|
|
515
|
-
}
|
|
516
|
-
async function inScratch(toolchain, command, timeoutMs) {
|
|
517
|
-
const root = await mkdtemp(join3(tmpdir(), `garuda-toolchain-${toolchain.id}-`));
|
|
518
|
-
try {
|
|
519
|
-
await writeFiles(root, toolchain.files);
|
|
520
|
-
const r = await new HostExecutor().run(command, {
|
|
521
|
-
root,
|
|
522
|
-
sandbox: false,
|
|
523
|
-
writePaths: [root],
|
|
524
|
-
denyWritePaths: [],
|
|
525
|
-
denyReadPaths: [],
|
|
526
|
-
network: true,
|
|
527
|
-
envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
|
|
528
|
-
timeoutMs,
|
|
529
|
-
maxOutputBytes: 4e3
|
|
530
|
-
});
|
|
531
|
-
const output = `${r.stdout.text}${r.stderr.text}`.trim();
|
|
532
|
-
return { ok: r.exitCode === 0 && !r.timedOut, output };
|
|
533
|
-
} catch (error) {
|
|
534
|
-
return { ok: false, output: error.message };
|
|
535
|
-
} finally {
|
|
536
|
-
await rm(root, { recursive: true, force: true });
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// src/evals/repoTasks.ts
|
|
541
|
-
import { mkdtempSync, realpathSync } from "fs";
|
|
542
|
-
import { mkdir, readFile as readFile2, rm as rm2, symlink, writeFile as writeFile2 } from "fs/promises";
|
|
543
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
544
|
-
import { dirname as dirname2, join as join4, sep } from "path";
|
|
545
|
-
import { z } from "zod";
|
|
546
|
-
var REPO_SUITE_FILE = join4(".garuda", "evals", "repo-suite.json");
|
|
547
|
-
var DEFAULT_COMMITS = 200;
|
|
548
|
-
var DEFAULT_MAX_TASKS = 30;
|
|
549
|
-
var MAX_TASK_FILES = 10;
|
|
550
|
-
var BUILD_TEST_TIMEOUT_MS = 5 * 6e4;
|
|
551
|
-
var testFile = z.object({ path: z.string(), content: z.string().nullable() });
|
|
552
|
-
var taskSchema = z.object({
|
|
553
|
-
id: z.string(),
|
|
554
|
-
sha: z.string(),
|
|
555
|
-
base: z.string(),
|
|
556
|
-
subject: z.string(),
|
|
557
|
-
prompt: z.string(),
|
|
558
|
-
tests: z.array(testFile),
|
|
559
|
-
/** The check: the test command for this task's test files. Absent: the suite's test command. */
|
|
560
|
-
check: z.string().optional()
|
|
561
|
-
});
|
|
562
|
-
var suiteSchema = z.object({
|
|
563
|
-
version: z.literal(1),
|
|
564
|
-
root: z.string(),
|
|
565
|
-
head: z.string(),
|
|
566
|
-
createdAt: z.string(),
|
|
567
|
-
testCommand: z.string(),
|
|
568
|
-
tasks: z.array(taskSchema)
|
|
569
|
-
});
|
|
570
|
-
var DOC_PATH = /\.(md|mdx|rst|txt|adoc)$|(^|\/)docs?\//i;
|
|
571
|
-
var PACKAGE_JSON = /(^|\/)package\.json$/;
|
|
572
|
-
function skipReason(commit) {
|
|
573
|
-
const { files } = commit;
|
|
574
|
-
if (files.length === 0) return "no files";
|
|
575
|
-
const code = files.filter((f) => !DOC_PATH.test(f.path));
|
|
576
|
-
if (code.length > MAX_TASK_FILES) return `more than ${MAX_TASK_FILES} files`;
|
|
577
|
-
if (files.some((f) => f.added === void 0)) return "a binary file";
|
|
578
|
-
if (files.some((f) => MANIFEST.test(f.path) && !PACKAGE_JSON.test(f.path))) {
|
|
579
|
-
return "dependencies changed";
|
|
580
|
-
}
|
|
581
|
-
const tests = code.filter((f) => TEST_PATH.test(f.path));
|
|
582
|
-
if (tests.length === 0) return "no test change";
|
|
583
|
-
if (tests.length === code.length) return "only tests changed";
|
|
584
|
-
return void 0;
|
|
585
|
-
}
|
|
586
|
-
function dependencyChange(diff) {
|
|
587
|
-
for (const line of diff.split("\n")) {
|
|
588
|
-
if (!/^[+-]/.test(line) || /^(\+\+\+|---)/.test(line)) continue;
|
|
589
|
-
if (/[dD]ependencies"/.test(line)) return true;
|
|
590
|
-
const entry = /^[+-]\s*"([^"]+)":\s*"[~^<>=]*\d/.exec(line);
|
|
591
|
-
if (entry !== null && entry[1] !== "version") return true;
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
function scopedTestCommand(command, files) {
|
|
596
|
-
const list = files.map(shellWord).join(" ");
|
|
597
|
-
if (command.includes("{files}")) return command.replaceAll("{files}", list);
|
|
598
|
-
if (files.length === 0) return command;
|
|
599
|
-
const c = command.trim();
|
|
600
|
-
if (/^npm (run )?test$/.test(c)) return `${c} -- ${list}`;
|
|
601
|
-
if (/^(pnpm|yarn)( run)? test$/.test(c) || /^node --test$/.test(c) || /^(npx |pnpm exec )?(vitest|jest)( run)?$/.test(c) || /-m pytest( -q)?$|^pytest( -q)?$/.test(c)) {
|
|
602
|
-
return `${c} ${list}`;
|
|
603
|
-
}
|
|
604
|
-
return c;
|
|
605
|
-
}
|
|
606
|
-
function repoPrompt(commit, tests) {
|
|
607
|
-
return [
|
|
608
|
-
commit.subject.trim(),
|
|
609
|
-
...commit.body.trim() === "" ? [] : ["", commit.body.trim()],
|
|
610
|
-
"",
|
|
611
|
-
`The tests for this change are already in the project: ${tests.join(", ")}. Make the change so that they pass. Do not change these test files.`
|
|
612
|
-
].join("\n");
|
|
613
|
-
}
|
|
614
|
-
async function recentCommits(executor, root, options = {}) {
|
|
615
|
-
const log = await git(executor, root, [
|
|
616
|
-
"log",
|
|
617
|
-
"--no-merges",
|
|
618
|
-
"--format=%H%x00%s%x00%b%x1e",
|
|
619
|
-
`-n${options.commits ?? DEFAULT_COMMITS}`,
|
|
620
|
-
...options.since === void 0 ? [] : [`--since=${options.since}`],
|
|
621
|
-
"HEAD"
|
|
622
|
-
]);
|
|
623
|
-
const out = [];
|
|
624
|
-
for (const entry of log.stdout.split("")) {
|
|
625
|
-
const [sha = "", subject = "", body = ""] = entry.replace(/^\n/, "").split("\0");
|
|
626
|
-
if (!/^[0-9a-f]{40,64}$/.test(sha)) continue;
|
|
627
|
-
const stat2 = await git(executor, root, ["show", "--no-renames", "--numstat", "--format=", sha]);
|
|
628
|
-
const files = stat2.stdout.split("\n").map((line) => /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line)).filter((m) => m !== null).map(
|
|
629
|
-
(m) => m[1] === "-" ? { path: m[3] } : { path: m[3], added: Number(m[1]), removed: Number(m[2]) }
|
|
630
|
-
);
|
|
631
|
-
out.push({ sha, subject, body, files });
|
|
632
|
-
}
|
|
633
|
-
return out;
|
|
634
|
-
}
|
|
635
|
-
async function taskWorktree(executor, root, commit) {
|
|
636
|
-
const dir = realpathSync(mkdtempSync(join4(tmpdir2(), "garuda-repo-task-")));
|
|
637
|
-
await git(executor, root, ["worktree", "add", "--detach", "--quiet", dir, commit]);
|
|
638
|
-
for (const name of LINK_CANDIDATES) {
|
|
639
|
-
try {
|
|
640
|
-
await symlink(join4(root, name), join4(dir, name), "dir");
|
|
641
|
-
} catch {
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
return dir;
|
|
645
|
-
}
|
|
646
|
-
async function isolatedTaskCopy(executor, root, commit) {
|
|
647
|
-
const dir = realpathSync(mkdtempSync(join4(tmpdir2(), "garuda-repo-task-")));
|
|
648
|
-
const indexDir = mkdtempSync(join4(tmpdir2(), "garuda-repo-index-"));
|
|
649
|
-
try {
|
|
650
|
-
const env = { GIT_INDEX_FILE: join4(indexDir, "index") };
|
|
651
|
-
await git(executor, root, ["read-tree", commit], { env });
|
|
652
|
-
await git(executor, root, ["checkout-index", "--all", "--force", `--prefix=${dir}/`], { env });
|
|
653
|
-
} finally {
|
|
654
|
-
await rm2(indexDir, { recursive: true, force: true });
|
|
655
|
-
}
|
|
656
|
-
await git(executor, dir, ["init", "--quiet"]);
|
|
657
|
-
await git(executor, dir, ["add", "--all"]);
|
|
658
|
-
await git(executor, dir, [
|
|
659
|
-
"-c",
|
|
660
|
-
"user.name=Garuda eval",
|
|
661
|
-
"-c",
|
|
662
|
-
"user.email=eval@garuda.invalid",
|
|
663
|
-
"commit",
|
|
664
|
-
"--quiet",
|
|
665
|
-
"--no-verify",
|
|
666
|
-
"--message",
|
|
667
|
-
"The task's start"
|
|
668
|
-
]);
|
|
669
|
-
for (const name of LINK_CANDIDATES) {
|
|
670
|
-
try {
|
|
671
|
-
await symlink(join4(root, name), join4(dir, name), "dir");
|
|
672
|
-
} catch {
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return dir;
|
|
676
|
-
}
|
|
677
|
-
function repoTaskReadDenials(root, home) {
|
|
678
|
-
const links = LINK_CANDIDATES.map((name) => join4(root, name));
|
|
679
|
-
const inHome = root === home || root.startsWith(`${home}${sep}`);
|
|
680
|
-
return [
|
|
681
|
-
...inHome ? denyAround(home, links, true) : [],
|
|
682
|
-
...inHome ? [] : denyAround(root, links)
|
|
683
|
-
];
|
|
684
|
-
}
|
|
685
|
-
async function removeTaskWorktree(executor, root, dir) {
|
|
686
|
-
await git(executor, root, ["worktree", "remove", "--force", dir], { check: false });
|
|
687
|
-
await rm2(dir, { recursive: true, force: true });
|
|
688
|
-
await git(executor, root, ["worktree", "prune"], { check: false });
|
|
689
|
-
}
|
|
690
|
-
async function applyTests(dir, tests) {
|
|
691
|
-
for (const t of tests) {
|
|
692
|
-
const file = join4(dir, t.path);
|
|
693
|
-
if (t.content === null) await rm2(file, { force: true });
|
|
694
|
-
else {
|
|
695
|
-
await mkdir(dirname2(file), { recursive: true });
|
|
696
|
-
await writeFile2(file, t.content);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
async function runTests(executor, dir, command) {
|
|
701
|
-
const result = await executor.run(command, {
|
|
702
|
-
root: dir,
|
|
703
|
-
sandbox: false,
|
|
704
|
-
writePaths: [dir],
|
|
705
|
-
denyWritePaths: [],
|
|
706
|
-
denyReadPaths: [],
|
|
707
|
-
network: false,
|
|
708
|
-
envAllowlist: [...DEFAULT_ENV_ALLOWLIST, ...TOOLCHAIN_ENV],
|
|
709
|
-
timeoutMs: BUILD_TEST_TIMEOUT_MS,
|
|
710
|
-
maxOutputBytes: 2e4
|
|
711
|
-
});
|
|
712
|
-
const output = `${result.stdout.text}${result.stderr.text}`;
|
|
713
|
-
return {
|
|
714
|
-
ok: result.exitCode === 0 && !result.timedOut,
|
|
715
|
-
tail: result.timedOut ? "(stopped: too long)" : tail(output, 15)
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
async function buildRepoSuite(executor, root, options) {
|
|
719
|
-
const say = options.onProgress ?? (() => {
|
|
720
|
-
});
|
|
721
|
-
const head = (await git(executor, root, ["rev-parse", "HEAD"])).stdout.trim();
|
|
722
|
-
const commits = await recentCommits(executor, root, {
|
|
723
|
-
...options.commits === void 0 ? {} : { commits: options.commits },
|
|
724
|
-
...options.since === void 0 ? {} : { since: options.since }
|
|
725
|
-
});
|
|
726
|
-
const skipped = {};
|
|
727
|
-
const skip = (why) => {
|
|
728
|
-
skipped[why] = (skipped[why] ?? 0) + 1;
|
|
729
|
-
};
|
|
730
|
-
const tasks = [];
|
|
731
|
-
let checked = 0;
|
|
732
|
-
let failure;
|
|
733
|
-
const max = options.maxTasks ?? DEFAULT_MAX_TASKS;
|
|
734
|
-
for (const commit of commits) {
|
|
735
|
-
if (tasks.length >= max) break;
|
|
736
|
-
const why = skipReason(commit);
|
|
737
|
-
if (why !== void 0) {
|
|
738
|
-
skip(why);
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
const parent = await git(
|
|
742
|
-
executor,
|
|
743
|
-
root,
|
|
744
|
-
["rev-parse", "--verify", "--quiet", `${commit.sha}^`],
|
|
745
|
-
{
|
|
746
|
-
check: false
|
|
747
|
-
}
|
|
748
|
-
);
|
|
749
|
-
if (parent.exitCode !== 0) {
|
|
750
|
-
skip("the first commit (no parent)");
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
if (commit.files.some((f) => PACKAGE_JSON.test(f.path))) {
|
|
754
|
-
const diff = await git(
|
|
755
|
-
executor,
|
|
756
|
-
root,
|
|
757
|
-
["show", "--format=", commit.sha, "--", "package.json"],
|
|
758
|
-
{
|
|
759
|
-
check: false
|
|
760
|
-
}
|
|
761
|
-
);
|
|
762
|
-
if (dependencyChange(diff.stdout)) {
|
|
763
|
-
skip("dependencies changed");
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const base = parent.stdout.trim();
|
|
768
|
-
const testPaths = commit.files.filter((f) => TEST_PATH.test(f.path) && !DOC_PATH.test(f.path)).map((f) => f.path);
|
|
769
|
-
const tests = [];
|
|
770
|
-
for (const path of testPaths) {
|
|
771
|
-
const shown = await git(executor, root, ["show", `${commit.sha}:${path}`], { check: false });
|
|
772
|
-
tests.push({ path, content: shown.exitCode === 0 ? shown.stdout : null });
|
|
773
|
-
}
|
|
774
|
-
const runnable = tests.filter((t) => t.content !== null).map((t) => t.path);
|
|
775
|
-
if (runnable.length === 0) {
|
|
776
|
-
skip("only deletes tests");
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
const check = scopedTestCommand(options.testCommand, runnable);
|
|
780
|
-
checked++;
|
|
781
|
-
say(`checking ${commit.sha.slice(0, 7)} ${commit.subject}`);
|
|
782
|
-
const dir = await taskWorktree(executor, root, base);
|
|
783
|
-
try {
|
|
784
|
-
await applyTests(dir, tests);
|
|
785
|
-
if ((await runTests(executor, dir, check)).ok) {
|
|
786
|
-
skip("the tests already pass at the parent");
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
await git(executor, dir, ["checkout", "--quiet", "--", "."], { check: false });
|
|
790
|
-
await git(executor, dir, cleanArgs(), { check: false });
|
|
791
|
-
await git(executor, dir, ["checkout", "--quiet", "--detach", commit.sha]);
|
|
792
|
-
const atCommit = await runTests(executor, dir, check);
|
|
793
|
-
if (!atCommit.ok) {
|
|
794
|
-
skip("the tests fail at the commit");
|
|
795
|
-
failure ??= { sha: commit.sha, command: check, tail: atCommit.tail };
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
} finally {
|
|
799
|
-
await removeTaskWorktree(executor, root, dir);
|
|
800
|
-
}
|
|
801
|
-
tasks.push({
|
|
802
|
-
id: `repo-${commit.sha.slice(0, 7)}`,
|
|
803
|
-
sha: commit.sha,
|
|
804
|
-
base,
|
|
805
|
-
subject: commit.subject,
|
|
806
|
-
prompt: repoPrompt(commit, testPaths),
|
|
807
|
-
tests,
|
|
808
|
-
check
|
|
809
|
-
});
|
|
810
|
-
say(`kept ${commit.sha.slice(0, 7)} (${tasks.length}/${max})`);
|
|
811
|
-
}
|
|
812
|
-
return {
|
|
813
|
-
suite: {
|
|
814
|
-
version: 1,
|
|
815
|
-
root,
|
|
816
|
-
head,
|
|
817
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
818
|
-
testCommand: options.testCommand,
|
|
819
|
-
tasks
|
|
820
|
-
},
|
|
821
|
-
checked,
|
|
822
|
-
skipped,
|
|
823
|
-
...failure === void 0 ? {} : { failure }
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
async function saveRepoSuite(suite) {
|
|
827
|
-
const file = join4(suite.root, REPO_SUITE_FILE);
|
|
828
|
-
await mkdir(dirname2(file), { recursive: true });
|
|
829
|
-
await writeFile2(file, `${JSON.stringify(suite, null, 2)}
|
|
830
|
-
`, { mode: 384 });
|
|
831
|
-
return file;
|
|
832
|
-
}
|
|
833
|
-
async function loadRepoSuite(root) {
|
|
834
|
-
let text;
|
|
835
|
-
try {
|
|
836
|
-
text = await readFile2(join4(root, REPO_SUITE_FILE), "utf8");
|
|
837
|
-
} catch {
|
|
838
|
-
return void 0;
|
|
839
|
-
}
|
|
840
|
-
const parsed = suiteSchema.safeParse(JSON.parse(text));
|
|
841
|
-
if (!parsed.success) throw new Error(`${REPO_SUITE_FILE}: ${z.prettifyError(parsed.error)}`);
|
|
842
|
-
return parsed.data;
|
|
843
|
-
}
|
|
844
|
-
function repoEvalTasks(suite) {
|
|
845
|
-
return suite.tasks.map((t) => ({
|
|
846
|
-
id: t.id,
|
|
847
|
-
title: t.subject,
|
|
848
|
-
prompt: t.prompt,
|
|
849
|
-
files: {},
|
|
850
|
-
check: t.check ?? suite.testCommand,
|
|
851
|
-
protect: t.tests.filter((f) => f.content !== null).map((f) => f.path),
|
|
852
|
-
solution: {},
|
|
853
|
-
repo: { root: suite.root, base: t.base, sha: t.sha, tests: t.tests }
|
|
854
|
-
}));
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
export {
|
|
858
|
-
DEFAULT_SEED,
|
|
859
|
-
shuffled,
|
|
860
|
-
byTask,
|
|
861
|
-
passInterval,
|
|
862
|
-
consistency,
|
|
863
|
-
spread,
|
|
864
|
-
stepsSd,
|
|
865
|
-
costPerPass,
|
|
866
|
-
unexpectedChanges,
|
|
867
|
-
mcpUse,
|
|
868
|
-
mcpUseText,
|
|
869
|
-
loadReport,
|
|
870
|
-
compareReports,
|
|
871
|
-
roughNote,
|
|
872
|
-
formatComparison,
|
|
873
|
-
writeFiles,
|
|
874
|
-
MAVEN_POM,
|
|
875
|
-
MAVEN_CHECK,
|
|
876
|
-
PYPROJECT,
|
|
877
|
-
PYTEST_CHECK,
|
|
878
|
-
isProtectedPath,
|
|
879
|
-
lines,
|
|
880
|
-
TOOLCHAIN_ENV,
|
|
881
|
-
TOOLCHAINS,
|
|
882
|
-
checkToolchains,
|
|
883
|
-
prepareToolchain,
|
|
884
|
-
toolchainId,
|
|
885
|
-
DEFAULT_COMMITS,
|
|
886
|
-
isolatedTaskCopy,
|
|
887
|
-
repoTaskReadDenials,
|
|
888
|
-
applyTests,
|
|
889
|
-
buildRepoSuite,
|
|
890
|
-
saveRepoSuite,
|
|
891
|
-
loadRepoSuite,
|
|
892
|
-
repoEvalTasks
|
|
893
|
-
};
|
|
894
|
-
//# sourceMappingURL=chunk-CWVXOSAA.js.map
|