@leostera/evalkit 0.7.0 → 0.9.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.
- package/CHANGELOG.md +56 -1
- package/README.md +2 -2
- package/dist/cli.mjs +386 -365
- package/dist/dashboard/assets/index-By1SJ1on.js +66 -0
- package/dist/dashboard/assets/index-geXMXCx9.css +2 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{files-C-U0-PWW.mjs → files-DkDQs2JL.mjs} +3 -0
- package/dist/{index-db-vbqEGFaU.mjs → index-db-BbVblaHz.mjs} +333 -51
- package/dist/index.d.mts +239 -178
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +246 -169
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +10 -3
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +6 -3
- package/dist/share.d.mts +49 -7
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +4 -4
- package/dist/skills/evalkit/SKILL.md +3 -3
- package/dist/skills/evalkit-agents/SKILL.md +5 -3
- package/dist/skills/evalkit-datasets/SKILL.md +5 -5
- package/dist/skills/evalkit-evals/SKILL.md +5 -5
- package/dist/skills/evalkit-judges/SKILL.md +5 -3
- package/dist/skills/evalkit-matrices/SKILL.md +2 -2
- package/dist/skills/evalkit-results/SKILL.md +6 -6
- package/dist/{src-CZnDXP1K.mjs → src-Bn6fnZnF.mjs} +8 -3
- package/dist/{src-b2Wnmzbb.mjs → src-BrfIXfO2.mjs} +75 -54
- package/dist/{src-DhromKNu.mjs → src-CcdLZqFk.mjs} +487 -250
- package/dist/upgrade-60SIRius.mjs +110 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BEBo_81Y.js +0 -34
- package/dist/dashboard/assets/index-C24IquYZ.css +0 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as upgradeManifest, i as upgradeEvent, o as upgradeStatus, r as upgradeCatalog } from "./upgrade-60SIRius.mjs";
|
|
1
2
|
import { Hono } from "hono";
|
|
2
3
|
//#region ../share/src/bundle.ts
|
|
3
4
|
/**
|
|
@@ -80,10 +81,127 @@ function escapeAttr(value) {
|
|
|
80
81
|
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
81
82
|
}
|
|
82
83
|
//#endregion
|
|
84
|
+
//#region ../share/src/legacy.ts
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
const parse = (data) => data ? JSON.parse(decoder.decode(data)) : void 0;
|
|
88
|
+
const encode = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
|
|
89
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90
|
+
/** True for a version 1 run.json: no `snapshotVersion`. */
|
|
91
|
+
function isLegacyRun(runJson) {
|
|
92
|
+
return isRecord(runJson) && runJson.snapshotVersion === void 0;
|
|
93
|
+
}
|
|
94
|
+
function upgradeSnapshotRun(files) {
|
|
95
|
+
const run = parse(files.get("run.json"));
|
|
96
|
+
if (!isRecord(run)) throw new Error("run.json must be an object");
|
|
97
|
+
const listed = parse(files.get("trials.json"));
|
|
98
|
+
const trials = (isRecord(listed) && Array.isArray(listed.trials) ? listed.trials : []).filter(isRecord);
|
|
99
|
+
const size = trials.length;
|
|
100
|
+
return trials.map((trial) => {
|
|
101
|
+
const id = size === 1 ? String(run.id) : String(trial.id);
|
|
102
|
+
const prefix = `trials/${String(trial.id)}/`;
|
|
103
|
+
const detail = parse(files.get(`${prefix}detail.json`));
|
|
104
|
+
const trialManifest = isRecord(detail) && isRecord(detail.manifest) ? detail.manifest : {};
|
|
105
|
+
const trialSummary = isRecord(detail) && isRecord(detail.summary) ? detail.summary : {};
|
|
106
|
+
const index = typeof trial.index === "number" ? trial.index : trials.indexOf(trial);
|
|
107
|
+
const batch = size > 1 ? {
|
|
108
|
+
id: String(run.id),
|
|
109
|
+
index,
|
|
110
|
+
size
|
|
111
|
+
} : void 0;
|
|
112
|
+
const scores = Array.isArray(trial.scores) ? trial.scores.filter(isRecord) : [];
|
|
113
|
+
const metrics = Array.isArray(trial.metrics) ? trial.metrics.filter(isRecord) : [];
|
|
114
|
+
const datasets = Array.isArray(trialManifest.datasets) ? trialManifest.datasets.filter(isRecord) : [];
|
|
115
|
+
const status = String(trial.status);
|
|
116
|
+
const pick = (keys) => Object.fromEntries(keys.flatMap((key) => run[key] === void 0 ? [] : [[key, run[key]]]));
|
|
117
|
+
const out = /* @__PURE__ */ new Map();
|
|
118
|
+
out.set("run.json", encode({
|
|
119
|
+
snapshotVersion: 2,
|
|
120
|
+
id,
|
|
121
|
+
...pick([
|
|
122
|
+
"evalId",
|
|
123
|
+
"suiteId",
|
|
124
|
+
"agent",
|
|
125
|
+
"matrixId",
|
|
126
|
+
"cellKey",
|
|
127
|
+
"parameters",
|
|
128
|
+
"archived",
|
|
129
|
+
"source"
|
|
130
|
+
]),
|
|
131
|
+
status,
|
|
132
|
+
startedAt: trial.startedAt ?? run.startedAt,
|
|
133
|
+
...trial.completedAt ? { completedAt: trial.completedAt } : {},
|
|
134
|
+
...trial.durationMs === void 0 ? {} : { durationMs: trial.durationMs },
|
|
135
|
+
...batch ? { batch } : {},
|
|
136
|
+
...trial.score === void 0 ? {} : { score: trial.score },
|
|
137
|
+
...scores.length ? { scorers: scores.map((score) => ({
|
|
138
|
+
name: score.name,
|
|
139
|
+
kind: score.kind,
|
|
140
|
+
...score.skipped ? { skipped: true } : {},
|
|
141
|
+
...score.value === void 0 || score.skipped ? {} : { value: score.value }
|
|
142
|
+
})) } : {},
|
|
143
|
+
...metrics.length ? { metrics: metrics.map((metric) => ({
|
|
144
|
+
name: metric.name,
|
|
145
|
+
...metric.unit ? { unit: metric.unit } : {},
|
|
146
|
+
...metric.better ? { better: metric.better } : {},
|
|
147
|
+
...metric.builtIn ? { builtIn: true } : {},
|
|
148
|
+
...metric.value === void 0 ? {} : { value: metric.value }
|
|
149
|
+
})) } : {},
|
|
150
|
+
...datasets.length ? { datasets: datasets.map((dataset) => ({
|
|
151
|
+
id: dataset.id,
|
|
152
|
+
digest: dataset.digest
|
|
153
|
+
})) } : {}
|
|
154
|
+
}));
|
|
155
|
+
const passed = trialSummary.scoring?.passed;
|
|
156
|
+
out.set("detail.json", encode({
|
|
157
|
+
scores,
|
|
158
|
+
...metrics.length ? { metrics } : {},
|
|
159
|
+
checkpoints: Array.isArray(trial.checkpoints) ? trial.checkpoints : [],
|
|
160
|
+
...trial.skippedScorers ? { skippedScorers: trial.skippedScorers } : {},
|
|
161
|
+
...trial.attachments ? { attachments: trial.attachments } : {},
|
|
162
|
+
manifest: upgradeManifest({
|
|
163
|
+
run: { ...pick([
|
|
164
|
+
"evalId",
|
|
165
|
+
"suiteId",
|
|
166
|
+
"parameters",
|
|
167
|
+
"source"
|
|
168
|
+
]) },
|
|
169
|
+
trial: trialManifest,
|
|
170
|
+
runId: id,
|
|
171
|
+
...batch ? { batch } : {},
|
|
172
|
+
status: upgradeStatus(trialSummary.status, passed)
|
|
173
|
+
}),
|
|
174
|
+
summary: {
|
|
175
|
+
...trialSummary,
|
|
176
|
+
status: upgradeStatus(trialSummary.status, passed)
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
for (const [path, data] of files) {
|
|
180
|
+
if (!path.startsWith(prefix)) continue;
|
|
181
|
+
const rest = path.slice(prefix.length);
|
|
182
|
+
if (rest === "detail.json") continue;
|
|
183
|
+
if (rest === "events.json") {
|
|
184
|
+
const events = parse(data);
|
|
185
|
+
out.set(rest, encode({ events: isRecord(events) && Array.isArray(events.events) ? events.events.map(upgradeEvent) : [] }));
|
|
186
|
+
} else out.set(rest, data);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
id,
|
|
190
|
+
files: out
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** A stored or uploaded version 1 catalog, as version 2 stores it. */
|
|
195
|
+
function upgradeCatalogFile(data) {
|
|
196
|
+
return encode(upgradeCatalog(parse(data)));
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
83
199
|
//#region ../share/src/paths.ts
|
|
84
200
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
201
|
+
const RUN_JSON = /^(detail|events|artifacts|workspace)\.json$/;
|
|
202
|
+
const RUN_FILE$1 = /^(?:workspace|attachments)\/(.+)$/;
|
|
203
|
+
const LEGACY_TRIAL_JSON = /^trials\/([^/]+)\/(detail|events|artifacts|workspace)\.json$/;
|
|
204
|
+
const LEGACY_TRIAL_FILE = /^trials\/([^/]+)\/(?:workspace|attachments)\/(.+)$/;
|
|
87
205
|
const isUuid = (value) => UUID.test(value);
|
|
88
206
|
const isSha = (value) => /^[0-9a-f]{40}$/.test(value);
|
|
89
207
|
/** One path segment that cannot escape or alias another location. */
|
|
@@ -91,14 +209,21 @@ function safeSegment(segment) {
|
|
|
91
209
|
return segment.length > 0 && segment.length <= 255 && segment !== "." && segment !== ".." && !/[\\/\0]/.test(segment);
|
|
92
210
|
}
|
|
93
211
|
/**
|
|
94
|
-
* The only files a run bundle may contain (
|
|
95
|
-
* `evalkit build`, for one run)
|
|
212
|
+
* The only files a run bundle may contain (snapshot format version 2, from
|
|
213
|
+
* `evalkit build`, for one run): its JSON, candidate workspace files and
|
|
214
|
+
* attachments. Never evaluator workspaces.
|
|
96
215
|
*/
|
|
97
216
|
function allowedRunPath(path) {
|
|
217
|
+
if (path === "run.json" || path === "catalog.json" || RUN_JSON.test(path)) return true;
|
|
218
|
+
const file = RUN_FILE$1.exec(path);
|
|
219
|
+
return Boolean(file && file[1].split("/").every(safeSegment));
|
|
220
|
+
}
|
|
221
|
+
/** The same for snapshot format version 1 (EvalKit before 0.9): a run of trials. */
|
|
222
|
+
function allowedLegacyRunPath(path) {
|
|
98
223
|
if (path === "run.json" || path === "catalog.json" || path === "trials.json") return true;
|
|
99
|
-
const json =
|
|
224
|
+
const json = LEGACY_TRIAL_JSON.exec(path);
|
|
100
225
|
if (json) return isUuid(json[1]);
|
|
101
|
-
const file =
|
|
226
|
+
const file = LEGACY_TRIAL_FILE.exec(path);
|
|
102
227
|
return Boolean(file && isUuid(file[1]) && file[2].split("/").every(safeSegment));
|
|
103
228
|
}
|
|
104
229
|
/**
|
|
@@ -116,26 +241,48 @@ function validRepoKey(key) {
|
|
|
116
241
|
//#region ../share/src/upload.ts
|
|
117
242
|
/** Rejected input: the client sent something the server will not store. */
|
|
118
243
|
var Rejected = class extends Error {};
|
|
244
|
+
/**
|
|
245
|
+
* A run bundle's runs. A bundle from EvalKit 0.9 or later is one run; one
|
|
246
|
+
* from an older CLI (snapshot format version 1, a run of trials) is
|
|
247
|
+
* converted into one run per trial, as `POST /api/v1/upgrade` converts
|
|
248
|
+
* stored runs (RFD0008).
|
|
249
|
+
*/
|
|
119
250
|
function parseUpload(bundle) {
|
|
120
251
|
const byPath = /* @__PURE__ */ new Map();
|
|
121
252
|
for (const file of bundle) {
|
|
122
|
-
if (!allowedRunPath(file.path)) throw new Rejected(`File not allowed in a run bundle: ${file.path}`);
|
|
123
253
|
if (byPath.has(file.path)) throw new Rejected(`Duplicate file in bundle: ${file.path}`);
|
|
124
254
|
byPath.set(file.path, file.data);
|
|
125
255
|
}
|
|
126
256
|
const runJson = byPath.get("run.json");
|
|
127
257
|
if (!runJson) throw new Rejected("Bundle has no run.json");
|
|
128
|
-
const
|
|
129
|
-
|
|
258
|
+
const legacy = isLegacyRun(parseJson(runJson, "run.json"));
|
|
259
|
+
const allowed = legacy ? allowedLegacyRunPath : allowedRunPath;
|
|
260
|
+
for (const path of byPath.keys()) if (!allowed(path)) throw new Rejected(`File not allowed in a run bundle: ${path}`);
|
|
261
|
+
const storedCatalog = byPath.get("catalog.json");
|
|
262
|
+
if (storedCatalog) parseCatalog(storedCatalog);
|
|
263
|
+
const catalog = storedCatalog && legacy ? upgradeCatalogFile(storedCatalog) : storedCatalog;
|
|
264
|
+
if (!legacy) return [runUpload(byPath, catalog)];
|
|
265
|
+
parseRun(runJson, { legacy: true });
|
|
266
|
+
return upgradeSnapshotRun(byPath).filter(({ files }) => {
|
|
267
|
+
const status = JSON.parse(new TextDecoder().decode(files.get("run.json"))).status;
|
|
268
|
+
return status === "passed" || status === "failed" || status === "errored";
|
|
269
|
+
}).map(({ files }) => runUpload(files, catalog));
|
|
270
|
+
}
|
|
271
|
+
function runUpload(files, catalog) {
|
|
272
|
+
const runJson = files.get("run.json");
|
|
130
273
|
return {
|
|
131
274
|
run: parseRun(runJson),
|
|
132
275
|
runJson,
|
|
133
276
|
...catalog ? { catalog } : {},
|
|
134
|
-
files:
|
|
277
|
+
files: [...files].filter(([path]) => path !== "run.json" && path !== "catalog.json").map(([path, data]) => ({
|
|
278
|
+
path,
|
|
279
|
+
data
|
|
280
|
+
}))
|
|
135
281
|
};
|
|
136
282
|
}
|
|
137
|
-
function parseRun(data) {
|
|
283
|
+
function parseRun(data, options = {}) {
|
|
138
284
|
const value = record(parseJson(data, "run.json"), "run.json");
|
|
285
|
+
if (!options.legacy && value.snapshotVersion !== 2) throw new Rejected("run.json must be snapshot format version 2");
|
|
139
286
|
const source = record(value.source, "run.json source");
|
|
140
287
|
const id = text(value.id, "id");
|
|
141
288
|
if (!isUuid(id)) throw new Rejected("run.json id must be a UUID");
|
|
@@ -206,6 +353,12 @@ function optionalText(value, key, name) {
|
|
|
206
353
|
}
|
|
207
354
|
//#endregion
|
|
208
355
|
//#region ../share/src/app.ts
|
|
356
|
+
/** `''` or `/a/b`: a path prefix with no trailing slash. */
|
|
357
|
+
function normalizeBasePath(basePath) {
|
|
358
|
+
const path = (basePath ?? "").replace(/\/+$/, "");
|
|
359
|
+
if (path && !/^(\/[A-Za-z0-9._~-]+)+$/.test(path)) throw new Error(`basePath must look like /evals (letters, digits, ".", "_", "~" and "-" between slashes), got ${JSON.stringify(basePath)}`);
|
|
360
|
+
return path;
|
|
361
|
+
}
|
|
209
362
|
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
210
363
|
const NO_CACHE = { "cache-control": "no-cache" };
|
|
211
364
|
const RUN_FILE = /^repos\/(.+)\/commits\/([0-9a-f]{40})\/runs\/([^/]+)\/run\.json$/;
|
|
@@ -221,6 +374,7 @@ const jsonLine = (event) => console.log(JSON.stringify(event));
|
|
|
221
374
|
*
|
|
222
375
|
* POST /api/v1/uploads one run bundle
|
|
223
376
|
* POST /api/v1/reindex rebuild the index from stored run.json files
|
|
377
|
+
* POST /api/v1/upgrade convert stored runs from before 0.9 (RFD0008)
|
|
224
378
|
* GET /api/v1/repos indexed repositories
|
|
225
379
|
* GET /api/v1/repos/<repo>/-/commits commits, newest first
|
|
226
380
|
* GET /api/v1/repos/<repo>/-/<sha>/data/… snapshot data
|
|
@@ -229,13 +383,24 @@ const jsonLine = (event) => console.log(JSON.stringify(event));
|
|
|
229
383
|
* GET /<repo>/-/<ref|prefix>/… → full SHA
|
|
230
384
|
* GET everything else the dashboard app
|
|
231
385
|
*
|
|
386
|
+
* All of it below `basePath` when set. With `repo` (single-repository mode)
|
|
387
|
+
* pages are `/` (→ latest commit) and `/-/<ref|prefix>/…`.
|
|
388
|
+
*
|
|
232
389
|
* There is no authentication: run it on a private network.
|
|
233
390
|
*/
|
|
234
391
|
function createShareApp(options) {
|
|
235
392
|
const { files, index, assets } = options;
|
|
236
393
|
const log = options.log ?? jsonLine;
|
|
237
394
|
const maxBundleBytes = options.maxBundleBytes ?? 268435456;
|
|
238
|
-
const
|
|
395
|
+
const base = normalizeBasePath(options.basePath);
|
|
396
|
+
const single = options.repo;
|
|
397
|
+
if (single !== void 0 && !validRepoKey(single)) throw new Error(`repo must be a repository key such as github.com/acme/agents, got ${JSON.stringify(single)}`);
|
|
398
|
+
/** Where a repository's pages live: `<base>/<repo>`, or `<base>` for the single one. */
|
|
399
|
+
const pagesOf = (repo) => single ? base : `${base}/${repo}`;
|
|
400
|
+
const shown = (repo) => !single || repo === single;
|
|
401
|
+
/** The request path below `basePath`. */
|
|
402
|
+
const local = (path) => path.slice(base.length);
|
|
403
|
+
const app = base ? new Hono().basePath(base) : new Hono();
|
|
239
404
|
app.onError((error, context) => {
|
|
240
405
|
log({
|
|
241
406
|
event: "share.error",
|
|
@@ -246,9 +411,12 @@ function createShareApp(options) {
|
|
|
246
411
|
return context.json({ error: "Internal error" }, 500);
|
|
247
412
|
});
|
|
248
413
|
app.post("/api/v1/uploads", async (context) => {
|
|
249
|
-
let
|
|
414
|
+
let uploads;
|
|
250
415
|
try {
|
|
251
|
-
|
|
416
|
+
uploads = parseUpload(await decodeBundle(new Uint8Array(await context.req.arrayBuffer()), { maxBytes: maxBundleBytes }));
|
|
417
|
+
if (!uploads.length) throw new Rejected("The bundle has no finished runs");
|
|
418
|
+
const other = uploads.find(({ run }) => !shown(run.repoKey));
|
|
419
|
+
if (other) throw new Rejected(`This share server only accepts results for ${single}, not ${other.run.repoKey}`);
|
|
252
420
|
} catch (error) {
|
|
253
421
|
if (!(error instanceof BundleError || error instanceof Rejected)) throw error;
|
|
254
422
|
log({
|
|
@@ -258,25 +426,107 @@ function createShareApp(options) {
|
|
|
258
426
|
});
|
|
259
427
|
return context.json({ error: error.message }, 400);
|
|
260
428
|
}
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
429
|
+
let created = false;
|
|
430
|
+
for (const upload of uploads) {
|
|
431
|
+
const { run } = upload;
|
|
432
|
+
const exists = await index.hasRun(run.id);
|
|
433
|
+
if (!exists) await storeRun(upload);
|
|
434
|
+
created ||= !exists;
|
|
435
|
+
log({
|
|
436
|
+
event: "share.upload",
|
|
437
|
+
outcome: exists ? "exists" : "created",
|
|
438
|
+
repo: run.repoKey,
|
|
439
|
+
commit: run.source.commit,
|
|
440
|
+
runId: run.id,
|
|
441
|
+
files: upload.files.length,
|
|
442
|
+
bytes: upload.files.reduce((sum, file) => sum + file.data.byteLength, 0)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const { run } = uploads[0];
|
|
446
|
+
const url = new URL(`${pagesOf(run.repoKey)}/-/${run.source.commit}/runs/${run.id}`, context.req.url).href;
|
|
275
447
|
return context.json({
|
|
276
|
-
status:
|
|
448
|
+
status: created ? "created" : "exists",
|
|
277
449
|
url
|
|
278
|
-
},
|
|
450
|
+
}, created ? 201 : 200);
|
|
279
451
|
});
|
|
452
|
+
/**
|
|
453
|
+
* Converts stored runs from before 0.9 (snapshot format version 1, a run of
|
|
454
|
+
* trials) into one run per trial, `limit` old runs per request (default
|
|
455
|
+
* 20), so it fits a Worker's limits. Call it until `remaining` is 0. New
|
|
456
|
+
* objects and index rows are written before old ones are removed, so an
|
|
457
|
+
* interrupted upgrade is finished by calling it again.
|
|
458
|
+
*/
|
|
459
|
+
app.post("/api/v1/upgrade", async (context) => {
|
|
460
|
+
const limit = Math.min(Math.max(Number(context.req.query("limit") ?? 20) || 20, 1), 200);
|
|
461
|
+
const legacy = [];
|
|
462
|
+
for await (const key of files.list("repos/")) {
|
|
463
|
+
if (!RUN_FILE.test(key)) continue;
|
|
464
|
+
const data = await readBytes(await files.get(key));
|
|
465
|
+
if (!data) continue;
|
|
466
|
+
let value;
|
|
467
|
+
try {
|
|
468
|
+
value = JSON.parse(new TextDecoder().decode(data));
|
|
469
|
+
} catch {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (isLegacyRun(value)) legacy.push(key);
|
|
473
|
+
}
|
|
474
|
+
let runs = 0;
|
|
475
|
+
const batch = legacy.slice(0, limit);
|
|
476
|
+
for (const key of batch) runs += await upgradeStoredRun(key);
|
|
477
|
+
const result = {
|
|
478
|
+
upgraded: batch.length,
|
|
479
|
+
runs,
|
|
480
|
+
remaining: legacy.length - batch.length
|
|
481
|
+
};
|
|
482
|
+
log({
|
|
483
|
+
event: "share.upgrade",
|
|
484
|
+
...result
|
|
485
|
+
});
|
|
486
|
+
return context.json(result);
|
|
487
|
+
});
|
|
488
|
+
/** One stored version 1 run → version 2 runs; returns how many. */
|
|
489
|
+
async function upgradeStoredRun(runJsonKey) {
|
|
490
|
+
const [, repo, sha, oldRunId] = RUN_FILE.exec(runJsonKey);
|
|
491
|
+
const commitBase = `repos/${repo}/commits/${sha}`;
|
|
492
|
+
const runBase = `${commitBase}/runs/${oldRunId}`;
|
|
493
|
+
const stored = /* @__PURE__ */ new Map();
|
|
494
|
+
const keys = [];
|
|
495
|
+
for await (const key of files.list(`${runBase}/`)) keys.push(key);
|
|
496
|
+
for (const key of keys) {
|
|
497
|
+
const data = await readBytes(await files.get(key));
|
|
498
|
+
if (data) stored.set(key.slice(runBase.length + 1), data);
|
|
499
|
+
}
|
|
500
|
+
let count = 0;
|
|
501
|
+
const written = /* @__PURE__ */ new Set();
|
|
502
|
+
let newRunJson;
|
|
503
|
+
for (const upgraded of upgradeSnapshotRun(stored)) {
|
|
504
|
+
const runJson = upgraded.files.get("run.json");
|
|
505
|
+
const run = storedRun(runJson);
|
|
506
|
+
if (!run) continue;
|
|
507
|
+
count += 1;
|
|
508
|
+
const inPlace = run.id === oldRunId;
|
|
509
|
+
if (!inPlace && await index.hasRun(run.id)) continue;
|
|
510
|
+
const base = `${commitBase}/runs/${run.id}`;
|
|
511
|
+
for (const [path, data] of upgraded.files) {
|
|
512
|
+
if (path === "run.json") continue;
|
|
513
|
+
await files.put(`${base}/${path}`, data);
|
|
514
|
+
written.add(`${base}/${path}`);
|
|
515
|
+
}
|
|
516
|
+
if (inPlace) {
|
|
517
|
+
await index.forgetRun(run.id);
|
|
518
|
+
newRunJson = runJson;
|
|
519
|
+
} else await files.put(`${base}/run.json`, runJson);
|
|
520
|
+
await index.recordRun(run);
|
|
521
|
+
}
|
|
522
|
+
const catalog = await readBytes(await files.get(`${commitBase}/catalog.json`));
|
|
523
|
+
if (catalog) await files.put(`${commitBase}/catalog.json`, upgradeCatalogFile(catalog));
|
|
524
|
+
if (!newRunJson) await index.forgetRun(oldRunId);
|
|
525
|
+
for (const key of keys) if (key !== runJsonKey && !written.has(key)) await files.delete(key);
|
|
526
|
+
if (newRunJson) await files.put(runJsonKey, newRunJson);
|
|
527
|
+
else await files.delete(runJsonKey);
|
|
528
|
+
return count;
|
|
529
|
+
}
|
|
280
530
|
/** Files first, index last: a run becomes visible only once it is indexed. */
|
|
281
531
|
async function storeRun({ run, runJson, catalog, files: runFiles }) {
|
|
282
532
|
const commitBase = `repos/${run.repoKey}/commits/${run.source.commit}`;
|
|
@@ -307,12 +557,12 @@ function createShareApp(options) {
|
|
|
307
557
|
indexed
|
|
308
558
|
});
|
|
309
559
|
});
|
|
310
|
-
app.get("/api/v1/repos", async (context) => context.json({ repos: await index.repos() }, 200, NO_CACHE));
|
|
560
|
+
app.get("/api/v1/repos", async (context) => context.json({ repos: (await index.repos()).filter((repo) => shown(repo.key)) }, 200, NO_CACHE));
|
|
311
561
|
app.get("/api/v1/repos/*", async (context) => {
|
|
312
|
-
const path = context.req.path.slice(14);
|
|
562
|
+
const path = local(context.req.path).slice(14);
|
|
313
563
|
const marker = path.indexOf("/-/");
|
|
314
564
|
const key = marker === -1 ? "" : path.slice(0, marker);
|
|
315
|
-
if (!validRepoKey(key)) return context.json({ error: "Unknown repository" }, 404);
|
|
565
|
+
if (!validRepoKey(key) || !shown(key)) return context.json({ error: "Unknown repository" }, 404);
|
|
316
566
|
const [sha = "", kind, ...segments] = path.slice(marker + 3).split("/");
|
|
317
567
|
if (sha === "commits" && kind === void 0) return context.json({
|
|
318
568
|
repo: key,
|
|
@@ -322,7 +572,7 @@ function createShareApp(options) {
|
|
|
322
572
|
return serveData(context, key, sha, segments.map(decodeURIComponent));
|
|
323
573
|
});
|
|
324
574
|
app.get("/assets/*", async (context) => {
|
|
325
|
-
const path = context.req.path.slice(8);
|
|
575
|
+
const path = local(context.req.path).slice(8);
|
|
326
576
|
if (!path.split("/").every(safeSegment)) return context.notFound();
|
|
327
577
|
const response = await assets.get(path);
|
|
328
578
|
if (!response) return context.notFound();
|
|
@@ -336,29 +586,47 @@ function createShareApp(options) {
|
|
|
336
586
|
app.all("/api/*", (context) => context.json({ error: "Not found" }, 404));
|
|
337
587
|
app.get("*", async (context) => {
|
|
338
588
|
const url = new URL(context.req.url);
|
|
339
|
-
const
|
|
589
|
+
const pathname = local(url.pathname);
|
|
590
|
+
const path = pathname.replace(/\/+$/, "");
|
|
591
|
+
if (single) {
|
|
592
|
+
if (path === `/${single}` || path.startsWith(`/${single}/`)) return context.redirect(`${base}${path.slice(single.length + 1) || "/"}${url.search}`);
|
|
593
|
+
if (!path) return latestPage(context, single);
|
|
594
|
+
if (!path.startsWith("/-/")) return dashboardPage(context, {
|
|
595
|
+
kind: "missing",
|
|
596
|
+
repo: single
|
|
597
|
+
});
|
|
598
|
+
return commitPage(context, single, pathname.slice(3), url.search);
|
|
599
|
+
}
|
|
340
600
|
const marker = path.indexOf("/-/");
|
|
341
|
-
if (marker === -1)
|
|
342
|
-
|
|
343
|
-
|
|
601
|
+
if (marker === -1) {
|
|
602
|
+
const repo = path.slice(1);
|
|
603
|
+
return repo ? latestPage(context, repo) : dashboardPage(context, { kind: "repositories" });
|
|
604
|
+
}
|
|
605
|
+
return commitPage(context, path.slice(1, marker), pathname.slice(marker + 3), url.search);
|
|
606
|
+
});
|
|
607
|
+
/**
|
|
608
|
+
* `<pages>/-/<ref>/…`: a short ref resolves to its full SHA first, so every
|
|
609
|
+
* commit page URL names one immutable commit.
|
|
610
|
+
*/
|
|
611
|
+
async function commitPage(context, repo, afterMarker, search) {
|
|
612
|
+
const [ref = "", ...rest] = afterMarker.split("/");
|
|
344
613
|
const sha = validRepoKey(repo) ? await index.resolve(repo, decodeURIComponent(ref)) : void 0;
|
|
345
614
|
if (!sha) return dashboardPage(context, {
|
|
346
615
|
kind: "missing",
|
|
347
616
|
repo,
|
|
348
617
|
ref
|
|
349
618
|
});
|
|
350
|
-
if (sha !== ref) return context.redirect(
|
|
619
|
+
if (sha !== ref) return context.redirect(`${pagesOf(repo)}/-/${sha}/${rest.join("/")}${search}`);
|
|
351
620
|
return dashboardPage(context, {
|
|
352
621
|
kind: "commit",
|
|
353
622
|
repo,
|
|
354
623
|
sha
|
|
355
624
|
});
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
async function
|
|
359
|
-
if (!repo) return dashboardPage(context, { kind: "repositories" });
|
|
625
|
+
}
|
|
626
|
+
/** A repository's own page opens its latest commit. */
|
|
627
|
+
async function latestPage(context, repo) {
|
|
360
628
|
const latest = validRepoKey(repo) ? await index.latest(repo) : void 0;
|
|
361
|
-
if (latest) return context.redirect(
|
|
629
|
+
if (latest) return context.redirect(`${pagesOf(repo)}/-/${latest}/`);
|
|
362
630
|
return dashboardPage(context, {
|
|
363
631
|
kind: "missing",
|
|
364
632
|
repo
|
|
@@ -366,14 +634,17 @@ function createShareApp(options) {
|
|
|
366
634
|
}
|
|
367
635
|
/** The dashboard's index.html, told where this page's data lives. */
|
|
368
636
|
async function dashboardPage(context, page) {
|
|
369
|
-
const api = page.kind === "repositories" ? void 0 :
|
|
637
|
+
const api = page.kind === "repositories" ? void 0 : `${base}/api/v1/repos/${page.repo}/-/`;
|
|
370
638
|
return context.html(dashboardIndex(await assets.index(), {
|
|
371
|
-
base: page.kind === "commit" ?
|
|
372
|
-
assets:
|
|
639
|
+
base: page.kind === "commit" ? `${pagesOf(page.repo)}/-/${page.sha}/` : `${base}/`,
|
|
640
|
+
assets: `${base}/assets/`,
|
|
373
641
|
share: {
|
|
374
|
-
repos:
|
|
642
|
+
repos: `${base}/api/v1/repos`,
|
|
643
|
+
home: `${base}/`,
|
|
644
|
+
...single ? { single: true } : {},
|
|
375
645
|
...page.kind === "repositories" ? {} : {
|
|
376
646
|
repo: page.repo,
|
|
647
|
+
pages: pagesOf(page.repo),
|
|
377
648
|
commits: `${api}commits`
|
|
378
649
|
},
|
|
379
650
|
...page.kind === "commit" ? { data: `${api}${page.sha}/` } : {},
|
|
@@ -395,11 +666,11 @@ function createShareApp(options) {
|
|
|
395
666
|
const catalog = data ? parseCatalog(data) : EMPTY_CATALOG;
|
|
396
667
|
return context.json(path === "catalog.json" ? { evals: catalog.catalog } : path === "evals.json" ? { evals: catalog.evals } : { suites: catalog.suites }, 200, NO_CACHE);
|
|
397
668
|
}
|
|
398
|
-
const runFile = /^runs\/([^/]+)\/(
|
|
669
|
+
const runFile = /^runs\/([^/]+)\/((?:detail|events|artifacts|workspace)\.json|(?:workspace|attachments)\/.+)$/.exec(path);
|
|
399
670
|
if (!runFile || !isUuid(runFile[1])) return context.notFound();
|
|
400
671
|
const body = await files.get(`${commitBase}/${path}`);
|
|
401
672
|
if (!body) return context.notFound();
|
|
402
|
-
const json = path.endsWith(".json") && !path.includes("/workspace/");
|
|
673
|
+
const json = path.endsWith(".json") && !path.includes("/workspace/") && !path.includes("/attachments/");
|
|
403
674
|
return new Response(body, { headers: json ? {
|
|
404
675
|
"content-type": "application/json",
|
|
405
676
|
"cache-control": IMMUTABLE
|
|
@@ -500,6 +771,16 @@ var ShareIndexDb = class {
|
|
|
500
771
|
WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0, run.status === "failed" || run.status === "errored" ? 1 : 0, run.repoKey, run.source.commit);
|
|
501
772
|
});
|
|
502
773
|
}
|
|
774
|
+
forgetRun(runId) {
|
|
775
|
+
this.sql.transaction(() => {
|
|
776
|
+
const [row] = this.sql.all("SELECT repo, sha, status, dirty FROM runs WHERE id = ?", runId);
|
|
777
|
+
if (!row) return;
|
|
778
|
+
this.sql.run("DELETE FROM runs WHERE id = ?", runId);
|
|
779
|
+
if (!row.dirty) this.sql.run(`UPDATE commits SET runs = runs - 1,
|
|
780
|
+
passed = passed - ?, failed = failed - ?
|
|
781
|
+
WHERE repo = ? AND sha = ?`, row.status === "passed" ? 1 : 0, row.status === "failed" || row.status === "errored" ? 1 : 0, row.repo, row.sha);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
503
784
|
repos() {
|
|
504
785
|
return this.sql.all(`SELECT key, url, project, first_seen_at AS firstSeenAt,
|
|
505
786
|
last_upload_at AS lastUploadAt,
|
|
@@ -535,6 +816,7 @@ function asyncIndex(db) {
|
|
|
535
816
|
return {
|
|
536
817
|
hasRun: async (runId) => db.hasRun(runId),
|
|
537
818
|
recordRun: async (run) => db.recordRun(run),
|
|
819
|
+
forgetRun: async (runId) => db.forgetRun(runId),
|
|
538
820
|
repos: async () => db.repos(),
|
|
539
821
|
commits: async (repo) => db.commits(repo),
|
|
540
822
|
runs: async (repo, sha) => db.runs(repo, sha),
|
|
@@ -543,4 +825,4 @@ function asyncIndex(db) {
|
|
|
543
825
|
};
|
|
544
826
|
}
|
|
545
827
|
//#endregion
|
|
546
|
-
export {
|
|
828
|
+
export { parseUpload as a, validRepoKey as c, decodeBundle as d, encodeBundle as f, normalizeBasePath as i, dashboardIndex as l, asyncIndex as n, allowedRunPath as o, createShareApp as r, repoKey as s, ShareIndexDb as t, BUNDLE_FORMAT as u };
|