@leostera/evalkit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +8 -7
- package/dist/cli.mjs +473 -72
- package/dist/cli.mjs.map +1 -1
- package/dist/dashboard/assets/index-D9NtA46z.css +2 -0
- package/dist/dashboard/assets/{index-CVd_Np_w.js → index-DrBwLKJD.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/files-C-U0-PWW.mjs +26 -0
- package/dist/files-C-U0-PWW.mjs.map +1 -0
- package/dist/{index-M5eRlMfd.d.mts → index-U6TUTBf7.d.mts} +87 -9
- package/dist/index-U6TUTBf7.d.mts.map +1 -0
- package/dist/index-db-HdZXelOS.d.mts +142 -0
- package/dist/index-db-HdZXelOS.d.mts.map +1 -0
- package/dist/index-db-vbqEGFaU.mjs +548 -0
- package/dist/index-db-vbqEGFaU.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/runner.d.mts +21 -3
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +1 -1
- package/dist/share-worker.d.mts +51 -0
- package/dist/share-worker.d.mts.map +1 -0
- package/dist/share-worker.mjs +73 -0
- package/dist/share-worker.mjs.map +1 -0
- package/dist/share.d.mts +116 -0
- package/dist/share.d.mts.map +1 -0
- package/dist/share.mjs +4 -0
- package/dist/{src-DrwwgaPn.mjs → src-ButXPY8P.mjs} +20 -5
- package/dist/src-ButXPY8P.mjs.map +1 -0
- package/dist/{src-DluldGKn.mjs → src-DoPs9Yug.mjs} +11 -7
- package/dist/src-DoPs9Yug.mjs.map +1 -0
- package/dist/src-DzZueFn3.mjs +98 -0
- package/dist/src-DzZueFn3.mjs.map +1 -0
- package/package.json +10 -1
- package/dist/dashboard/assets/index-CMr4aulR.css +0 -2
- package/dist/index-M5eRlMfd.d.mts.map +0 -1
- package/dist/src-DluldGKn.mjs.map +0 -1
- package/dist/src-DrwwgaPn.mjs.map +0 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
//#region ../share/src/bundle.ts
|
|
3
|
+
/**
|
|
4
|
+
* A run bundle: the snapshot files for one run, as one gzip-compressed body.
|
|
5
|
+
*
|
|
6
|
+
* "EKBUNDLE" (8 bytes) · header length (u32 BE) · header JSON · file bytes…
|
|
7
|
+
* header = { format: 1, files: [{ path, size }, …] }, bytes in that order.
|
|
8
|
+
*
|
|
9
|
+
* Both ends are EvalKit, so this is simpler than tar and has no path-length limit.
|
|
10
|
+
*/
|
|
11
|
+
const BUNDLE_FORMAT = 1;
|
|
12
|
+
const MAGIC = new TextEncoder().encode("EKBUNDLE");
|
|
13
|
+
async function pipe(data, transform) {
|
|
14
|
+
const stream = new Blob([data]).stream().pipeThrough(transform);
|
|
15
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
16
|
+
}
|
|
17
|
+
async function encodeBundle(files) {
|
|
18
|
+
const header = new TextEncoder().encode(JSON.stringify({
|
|
19
|
+
format: 1,
|
|
20
|
+
files: files.map(({ path, data }) => ({
|
|
21
|
+
path,
|
|
22
|
+
size: data.byteLength
|
|
23
|
+
}))
|
|
24
|
+
}));
|
|
25
|
+
const total = MAGIC.length + 4 + header.length + files.reduce((sum, file) => sum + file.data.byteLength, 0);
|
|
26
|
+
const raw = new Uint8Array(total);
|
|
27
|
+
raw.set(MAGIC, 0);
|
|
28
|
+
new DataView(raw.buffer).setUint32(MAGIC.length, header.length);
|
|
29
|
+
raw.set(header, MAGIC.length + 4);
|
|
30
|
+
let offset = MAGIC.length + 4 + header.length;
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
raw.set(file.data, offset);
|
|
33
|
+
offset += file.data.byteLength;
|
|
34
|
+
}
|
|
35
|
+
return pipe(raw, new CompressionStream("gzip"));
|
|
36
|
+
}
|
|
37
|
+
var BundleError = class extends Error {};
|
|
38
|
+
/** Decode and check the framing; path rules are enforced by the caller. */
|
|
39
|
+
async function decodeBundle(body, limits) {
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = await pipe(body, new DecompressionStream("gzip"));
|
|
43
|
+
} catch {
|
|
44
|
+
throw new BundleError("Body is not a gzip-compressed EvalKit bundle");
|
|
45
|
+
}
|
|
46
|
+
if (raw.byteLength > limits.maxBytes) throw new BundleError(`Bundle exceeds ${limits.maxBytes} bytes`);
|
|
47
|
+
if (raw.byteLength < MAGIC.length + 4 || MAGIC.some((byte, index) => raw[index] !== byte)) throw new BundleError("Not an EvalKit bundle");
|
|
48
|
+
const headerLength = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(MAGIC.length);
|
|
49
|
+
const start = MAGIC.length + 4;
|
|
50
|
+
let header;
|
|
51
|
+
try {
|
|
52
|
+
header = JSON.parse(new TextDecoder().decode(raw.subarray(start, start + headerLength)));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new BundleError("Bundle header is not valid JSON");
|
|
55
|
+
}
|
|
56
|
+
if (header.format !== 1) throw new BundleError(`Unsupported bundle format ${String(header.format)}`);
|
|
57
|
+
if (!Array.isArray(header.files)) throw new BundleError("Bundle header has no file list");
|
|
58
|
+
let offset = start + headerLength;
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const entry of header.files) {
|
|
61
|
+
if (typeof entry.path !== "string" || typeof entry.size !== "number" || !Number.isSafeInteger(entry.size) || entry.size < 0 || offset + entry.size > raw.byteLength) throw new BundleError("Bundle file list does not match its contents");
|
|
62
|
+
files.push({
|
|
63
|
+
path: entry.path,
|
|
64
|
+
data: raw.subarray(offset, offset + entry.size)
|
|
65
|
+
});
|
|
66
|
+
offset += entry.size;
|
|
67
|
+
}
|
|
68
|
+
if (offset !== raw.byteLength) throw new BundleError("Bundle has trailing bytes");
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region ../share/src/html.ts
|
|
73
|
+
function dashboardIndex(html, options) {
|
|
74
|
+
const attrs = ["data-evalkit-snapshot=\"static\"", ...options.share ? [`data-evalkit-share="${escapeAttr(JSON.stringify(options.share))}"`] : []].join(" ");
|
|
75
|
+
const result = html.replace("<html lang=\"en\">", `<html lang="en" ${attrs}>`).replace("<head>", `<head>\n <base href="${escapeAttr(options.base)}" />`).replaceAll("\"/assets/", `"${options.assets ?? "./assets/"}`);
|
|
76
|
+
if (!result.includes("data-evalkit-snapshot=\"static\"") || !result.includes("<base href=")) throw new Error("Bundled dashboard index.html has an unexpected shape");
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function escapeAttr(value) {
|
|
80
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region ../share/src/paths.ts
|
|
84
|
+
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 TRIAL_JSON = /^trials\/([^/]+)\/(detail|events|artifacts|workspace)\.json$/;
|
|
86
|
+
const TRIAL_FILE = /^trials\/([^/]+)\/workspace\/(.+)$/;
|
|
87
|
+
const isUuid = (value) => UUID.test(value);
|
|
88
|
+
const isSha = (value) => /^[0-9a-f]{40}$/.test(value);
|
|
89
|
+
/** One path segment that cannot escape or alias another location. */
|
|
90
|
+
function safeSegment(segment) {
|
|
91
|
+
return segment.length > 0 && segment.length <= 255 && segment !== "." && segment !== ".." && !/[\\/\0]/.test(segment);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The only files a run bundle may contain (the snapshot format from
|
|
95
|
+
* `evalkit build`, for one run). Never evaluator workspaces.
|
|
96
|
+
*/
|
|
97
|
+
function allowedRunPath(path) {
|
|
98
|
+
if (path === "run.json" || path === "catalog.json" || path === "trials.json") return true;
|
|
99
|
+
const json = TRIAL_JSON.exec(path);
|
|
100
|
+
if (json) return isUuid(json[1]);
|
|
101
|
+
const file = TRIAL_FILE.exec(path);
|
|
102
|
+
return Boolean(file && isUuid(file[1]) && file[2].split("/").every(safeSegment));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Repository key: normalized host/path, plus `/~<project>` for an eval
|
|
106
|
+
* project below the repository root. Used in storage keys and URLs.
|
|
107
|
+
*/
|
|
108
|
+
function repoKey(source) {
|
|
109
|
+
return source.project ? `${source.repo}/~${source.project}` : source.repo;
|
|
110
|
+
}
|
|
111
|
+
function validRepoKey(key) {
|
|
112
|
+
const segments = key.split("/");
|
|
113
|
+
return segments.length >= 2 && !segments.includes("-") && segments.every(safeSegment);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../share/src/upload.ts
|
|
117
|
+
/** Rejected input: the client sent something the server will not store. */
|
|
118
|
+
var Rejected = class extends Error {};
|
|
119
|
+
function parseUpload(bundle) {
|
|
120
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const file of bundle) {
|
|
122
|
+
if (!allowedRunPath(file.path)) throw new Rejected(`File not allowed in a run bundle: ${file.path}`);
|
|
123
|
+
if (byPath.has(file.path)) throw new Rejected(`Duplicate file in bundle: ${file.path}`);
|
|
124
|
+
byPath.set(file.path, file.data);
|
|
125
|
+
}
|
|
126
|
+
const runJson = byPath.get("run.json");
|
|
127
|
+
if (!runJson) throw new Rejected("Bundle has no run.json");
|
|
128
|
+
const catalog = byPath.get("catalog.json");
|
|
129
|
+
if (catalog) parseCatalog(catalog);
|
|
130
|
+
return {
|
|
131
|
+
run: parseRun(runJson),
|
|
132
|
+
runJson,
|
|
133
|
+
...catalog ? { catalog } : {},
|
|
134
|
+
files: bundle.filter((file) => file.path !== "run.json" && file.path !== "catalog.json")
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function parseRun(data) {
|
|
138
|
+
const value = record(parseJson(data, "run.json"), "run.json");
|
|
139
|
+
const source = record(value.source, "run.json source");
|
|
140
|
+
const id = text(value.id, "id");
|
|
141
|
+
if (!isUuid(id)) throw new Rejected("run.json id must be a UUID");
|
|
142
|
+
const commit = text(source.commit, "source.commit");
|
|
143
|
+
if (!isSha(commit)) throw new Rejected("source.commit must be a full 40-character SHA");
|
|
144
|
+
if (typeof source.dirty !== "boolean") throw new Rejected("source.dirty must be a boolean");
|
|
145
|
+
const status = value.status;
|
|
146
|
+
if (status !== "passed" && status !== "failed" && status !== "errored") throw new Rejected(`Only finished runs can be shared (status ${JSON.stringify(status)})`);
|
|
147
|
+
if (value.score !== void 0 && typeof value.score !== "number") throw new Rejected("score must be a number");
|
|
148
|
+
const shared = {
|
|
149
|
+
repo: text(source.repo, "source.repo"),
|
|
150
|
+
project: projectPath(source.project),
|
|
151
|
+
commit,
|
|
152
|
+
dirty: source.dirty,
|
|
153
|
+
...optionalText(source.ref, "ref", "source.ref"),
|
|
154
|
+
...optionalText(source.committedAt, "committedAt", "source.committedAt")
|
|
155
|
+
};
|
|
156
|
+
const key = repoKey(shared);
|
|
157
|
+
if (!validRepoKey(key)) throw new Rejected(`Invalid repository: ${key}`);
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
evalId: text(value.evalId, "evalId"),
|
|
161
|
+
status,
|
|
162
|
+
startedAt: text(value.startedAt, "startedAt"),
|
|
163
|
+
source: shared,
|
|
164
|
+
repoKey: key,
|
|
165
|
+
summary: value,
|
|
166
|
+
...optionalText(value.suiteId, "suiteId", "suiteId"),
|
|
167
|
+
...typeof value.score === "number" ? { score: value.score } : {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function parseCatalog(data) {
|
|
171
|
+
const value = record(parseJson(data, "catalog.json"), "catalog.json");
|
|
172
|
+
const list = (field) => {
|
|
173
|
+
const items = value[field];
|
|
174
|
+
if (!Array.isArray(items)) throw new Rejected(`catalog.json ${field} must be a list`);
|
|
175
|
+
return items;
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
catalog: list("catalog"),
|
|
179
|
+
evals: list("evals"),
|
|
180
|
+
suites: list("suites")
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The eval project's path in the repository; `''` at the root. */
|
|
184
|
+
function projectPath(value) {
|
|
185
|
+
if (typeof value !== "string") throw new Rejected("source.project must be a string");
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
function parseJson(data, name) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
191
|
+
} catch {
|
|
192
|
+
throw new Rejected(`${name} is not valid JSON`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function record(value, name) {
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Rejected(`${name} must be an object`);
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function text(value, name) {
|
|
200
|
+
if (typeof value !== "string" || !value) throw new Rejected(`${name} must be a non-empty string`);
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function optionalText(value, key, name) {
|
|
204
|
+
if (value === void 0 || value === null || value === "") return {};
|
|
205
|
+
return { [key]: text(value, name) };
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region ../share/src/app.ts
|
|
209
|
+
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
210
|
+
const NO_CACHE = { "cache-control": "no-cache" };
|
|
211
|
+
const RUN_FILE = /^repos\/(.+)\/commits\/([0-9a-f]{40})\/runs\/([^/]+)\/run\.json$/;
|
|
212
|
+
const EMPTY_CATALOG = {
|
|
213
|
+
catalog: [],
|
|
214
|
+
evals: [],
|
|
215
|
+
suites: []
|
|
216
|
+
};
|
|
217
|
+
const jsonLine = (event) => console.log(JSON.stringify(event));
|
|
218
|
+
/**
|
|
219
|
+
* The share server: a small JSON API plus the dashboard app. It stores
|
|
220
|
+
* uploaded run bundles and shows one read-only snapshot per repository commit.
|
|
221
|
+
*
|
|
222
|
+
* POST /api/v1/uploads one run bundle
|
|
223
|
+
* POST /api/v1/reindex rebuild the index from stored run.json files
|
|
224
|
+
* GET /api/v1/repos indexed repositories
|
|
225
|
+
* GET /api/v1/repos/<repo>/-/commits commits, newest first
|
|
226
|
+
* GET /api/v1/repos/<repo>/-/<sha>/data/… snapshot data
|
|
227
|
+
* GET /assets/* dashboard JS/CSS
|
|
228
|
+
* GET /<repo> → latest commit
|
|
229
|
+
* GET /<repo>/-/<ref|prefix>/… → full SHA
|
|
230
|
+
* GET everything else the dashboard app
|
|
231
|
+
*
|
|
232
|
+
* There is no authentication: run it on a private network.
|
|
233
|
+
*/
|
|
234
|
+
function createShareApp(options) {
|
|
235
|
+
const { files, index, assets } = options;
|
|
236
|
+
const log = options.log ?? jsonLine;
|
|
237
|
+
const maxBundleBytes = options.maxBundleBytes ?? 268435456;
|
|
238
|
+
const app = new Hono();
|
|
239
|
+
app.onError((error, context) => {
|
|
240
|
+
log({
|
|
241
|
+
event: "share.error",
|
|
242
|
+
method: context.req.method,
|
|
243
|
+
path: context.req.path,
|
|
244
|
+
error: error instanceof Error ? error.message : String(error)
|
|
245
|
+
});
|
|
246
|
+
return context.json({ error: "Internal error" }, 500);
|
|
247
|
+
});
|
|
248
|
+
app.post("/api/v1/uploads", async (context) => {
|
|
249
|
+
let upload;
|
|
250
|
+
try {
|
|
251
|
+
upload = parseUpload(await decodeBundle(new Uint8Array(await context.req.arrayBuffer()), { maxBytes: maxBundleBytes }));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (!(error instanceof BundleError || error instanceof Rejected)) throw error;
|
|
254
|
+
log({
|
|
255
|
+
event: "share.upload",
|
|
256
|
+
outcome: "rejected",
|
|
257
|
+
reason: error.message
|
|
258
|
+
});
|
|
259
|
+
return context.json({ error: error.message }, 400);
|
|
260
|
+
}
|
|
261
|
+
const { run } = upload;
|
|
262
|
+
const url = new URL(`/${run.repoKey}/-/${run.source.commit}/runs/${run.id}`, context.req.url).href;
|
|
263
|
+
const exists = await index.hasRun(run.id);
|
|
264
|
+
if (!exists) await storeRun(upload);
|
|
265
|
+
const outcome = exists ? "exists" : "created";
|
|
266
|
+
log({
|
|
267
|
+
event: "share.upload",
|
|
268
|
+
outcome,
|
|
269
|
+
repo: run.repoKey,
|
|
270
|
+
commit: run.source.commit,
|
|
271
|
+
runId: run.id,
|
|
272
|
+
files: upload.files.length,
|
|
273
|
+
bytes: upload.files.reduce((sum, file) => sum + file.data.byteLength, 0)
|
|
274
|
+
});
|
|
275
|
+
return context.json({
|
|
276
|
+
status: outcome,
|
|
277
|
+
url
|
|
278
|
+
}, outcome === "created" ? 201 : 200);
|
|
279
|
+
});
|
|
280
|
+
/** Files first, index last: a run becomes visible only once it is indexed. */
|
|
281
|
+
async function storeRun({ run, runJson, catalog, files: runFiles }) {
|
|
282
|
+
const commitBase = `repos/${run.repoKey}/commits/${run.source.commit}`;
|
|
283
|
+
const runBase = `${commitBase}/runs/${run.id}`;
|
|
284
|
+
for (const file of runFiles) await files.put(`${runBase}/${file.path}`, file.data);
|
|
285
|
+
if (catalog && (!run.source.dirty || !await files.get(`${commitBase}/catalog.json`))) await files.put(`${commitBase}/catalog.json`, catalog);
|
|
286
|
+
await files.put(`${runBase}/run.json`, runJson);
|
|
287
|
+
await index.recordRun(run);
|
|
288
|
+
}
|
|
289
|
+
app.post("/api/v1/reindex", async (context) => {
|
|
290
|
+
let runs = 0;
|
|
291
|
+
let indexed = 0;
|
|
292
|
+
for await (const key of files.list("repos/")) {
|
|
293
|
+
if (!RUN_FILE.test(key)) continue;
|
|
294
|
+
runs += 1;
|
|
295
|
+
const run = storedRun(await readBytes(await files.get(key)));
|
|
296
|
+
if (!run || await index.hasRun(run.id)) continue;
|
|
297
|
+
await index.recordRun(run);
|
|
298
|
+
indexed += 1;
|
|
299
|
+
}
|
|
300
|
+
log({
|
|
301
|
+
event: "share.reindex",
|
|
302
|
+
runs,
|
|
303
|
+
indexed
|
|
304
|
+
});
|
|
305
|
+
return context.json({
|
|
306
|
+
runs,
|
|
307
|
+
indexed
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
app.get("/api/v1/repos", async (context) => context.json({ repos: await index.repos() }, 200, NO_CACHE));
|
|
311
|
+
app.get("/api/v1/repos/*", async (context) => {
|
|
312
|
+
const path = context.req.path.slice(14);
|
|
313
|
+
const marker = path.indexOf("/-/");
|
|
314
|
+
const key = marker === -1 ? "" : path.slice(0, marker);
|
|
315
|
+
if (!validRepoKey(key)) return context.json({ error: "Unknown repository" }, 404);
|
|
316
|
+
const [sha = "", kind, ...segments] = path.slice(marker + 3).split("/");
|
|
317
|
+
if (sha === "commits" && kind === void 0) return context.json({
|
|
318
|
+
repo: key,
|
|
319
|
+
commits: await index.commits(key)
|
|
320
|
+
}, 200, NO_CACHE);
|
|
321
|
+
if (!isSha(sha) || kind !== "data") return context.json({ error: "Not found" }, 404);
|
|
322
|
+
return serveData(context, key, sha, segments.map(decodeURIComponent));
|
|
323
|
+
});
|
|
324
|
+
app.get("/assets/*", async (context) => {
|
|
325
|
+
const path = context.req.path.slice(8);
|
|
326
|
+
if (!path.split("/").every(safeSegment)) return context.notFound();
|
|
327
|
+
const response = await assets.get(path);
|
|
328
|
+
if (!response) return context.notFound();
|
|
329
|
+
const headers = new Headers(response.headers);
|
|
330
|
+
headers.set("cache-control", IMMUTABLE);
|
|
331
|
+
return new Response(response.body, {
|
|
332
|
+
status: response.status,
|
|
333
|
+
headers
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
app.all("/api/*", (context) => context.json({ error: "Not found" }, 404));
|
|
337
|
+
app.get("*", async (context) => {
|
|
338
|
+
const url = new URL(context.req.url);
|
|
339
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
340
|
+
const marker = path.indexOf("/-/");
|
|
341
|
+
if (marker === -1) return repositoryPage(context, path.slice(1));
|
|
342
|
+
const repo = path.slice(1, marker);
|
|
343
|
+
const [ref = "", ...rest] = url.pathname.slice(marker + 3).split("/");
|
|
344
|
+
const sha = validRepoKey(repo) ? await index.resolve(repo, decodeURIComponent(ref)) : void 0;
|
|
345
|
+
if (!sha) return dashboardPage(context, {
|
|
346
|
+
kind: "missing",
|
|
347
|
+
repo,
|
|
348
|
+
ref
|
|
349
|
+
});
|
|
350
|
+
if (sha !== ref) return context.redirect(`/${repo}/-/${sha}/${rest.join("/")}${url.search}`);
|
|
351
|
+
return dashboardPage(context, {
|
|
352
|
+
kind: "commit",
|
|
353
|
+
repo,
|
|
354
|
+
sha
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
/** `/` lists repositories; `/<repo>` opens its latest commit. */
|
|
358
|
+
async function repositoryPage(context, repo) {
|
|
359
|
+
if (!repo) return dashboardPage(context, { kind: "repositories" });
|
|
360
|
+
const latest = validRepoKey(repo) ? await index.latest(repo) : void 0;
|
|
361
|
+
if (latest) return context.redirect(`/${repo}/-/${latest}/`);
|
|
362
|
+
return dashboardPage(context, {
|
|
363
|
+
kind: "missing",
|
|
364
|
+
repo
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/** The dashboard's index.html, told where this page's data lives. */
|
|
368
|
+
async function dashboardPage(context, page) {
|
|
369
|
+
const api = page.kind === "repositories" ? void 0 : `/api/v1/repos/${page.repo}/-/`;
|
|
370
|
+
return context.html(dashboardIndex(await assets.index(), {
|
|
371
|
+
base: page.kind === "commit" ? `/${page.repo}/-/${page.sha}/` : "/",
|
|
372
|
+
assets: "/assets/",
|
|
373
|
+
share: {
|
|
374
|
+
repos: "/api/v1/repos",
|
|
375
|
+
...page.kind === "repositories" ? {} : {
|
|
376
|
+
repo: page.repo,
|
|
377
|
+
commits: `${api}commits`
|
|
378
|
+
},
|
|
379
|
+
...page.kind === "commit" ? { data: `${api}${page.sha}/` } : {},
|
|
380
|
+
...page.kind === "missing" && page.ref ? { missing: page.ref } : {}
|
|
381
|
+
}
|
|
382
|
+
}), page.kind === "missing" ? 404 : 200, NO_CACHE);
|
|
383
|
+
}
|
|
384
|
+
async function serveData(context, key, sha, segments) {
|
|
385
|
+
if (!segments.every(safeSegment)) return context.notFound();
|
|
386
|
+
const path = segments.join("/");
|
|
387
|
+
const commitBase = `repos/${key}/commits/${sha}`;
|
|
388
|
+
if (path === "runs.json") return context.json({ runs: await index.runs(key, sha) }, 200, NO_CACHE);
|
|
389
|
+
if ([
|
|
390
|
+
"catalog.json",
|
|
391
|
+
"evals.json",
|
|
392
|
+
"suites.json"
|
|
393
|
+
].includes(path)) {
|
|
394
|
+
const data = await readBytes(await files.get(`${commitBase}/catalog.json`));
|
|
395
|
+
const catalog = data ? parseCatalog(data) : EMPTY_CATALOG;
|
|
396
|
+
return context.json(path === "catalog.json" ? { evals: catalog.catalog } : path === "evals.json" ? { evals: catalog.evals } : { suites: catalog.suites }, 200, NO_CACHE);
|
|
397
|
+
}
|
|
398
|
+
const runFile = /^runs\/([^/]+)\/(trials\.json|trials\/.+)$/.exec(path);
|
|
399
|
+
if (!runFile || !isUuid(runFile[1])) return context.notFound();
|
|
400
|
+
const body = await files.get(`${commitBase}/${path}`);
|
|
401
|
+
if (!body) return context.notFound();
|
|
402
|
+
const json = path.endsWith(".json") && !path.includes("/workspace/");
|
|
403
|
+
return new Response(body, { headers: json ? {
|
|
404
|
+
"content-type": "application/json",
|
|
405
|
+
"cache-control": IMMUTABLE
|
|
406
|
+
} : {
|
|
407
|
+
"content-type": "application/octet-stream",
|
|
408
|
+
"x-content-type-options": "nosniff",
|
|
409
|
+
"content-security-policy": "sandbox",
|
|
410
|
+
"cache-control": IMMUTABLE
|
|
411
|
+
} });
|
|
412
|
+
}
|
|
413
|
+
return app;
|
|
414
|
+
}
|
|
415
|
+
/** A stored run.json, or undefined if it is not a shareable run. */
|
|
416
|
+
function storedRun(data) {
|
|
417
|
+
if (!data) return void 0;
|
|
418
|
+
try {
|
|
419
|
+
return parseRun(data);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error instanceof Rejected) return void 0;
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function readBytes(body) {
|
|
426
|
+
if (!body) return null;
|
|
427
|
+
if (body instanceof Uint8Array) return body;
|
|
428
|
+
return new Uint8Array(await new Response(body).arrayBuffer());
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region ../share/src/index-db.ts
|
|
432
|
+
const MIGRATIONS = [
|
|
433
|
+
`CREATE TABLE IF NOT EXISTS repos (
|
|
434
|
+
key TEXT PRIMARY KEY,
|
|
435
|
+
url TEXT NOT NULL,
|
|
436
|
+
project TEXT NOT NULL,
|
|
437
|
+
first_seen_at TEXT NOT NULL,
|
|
438
|
+
last_upload_at TEXT NOT NULL
|
|
439
|
+
)`,
|
|
440
|
+
`CREATE TABLE IF NOT EXISTS commits (
|
|
441
|
+
repo TEXT NOT NULL,
|
|
442
|
+
sha TEXT NOT NULL,
|
|
443
|
+
ref TEXT,
|
|
444
|
+
committed_at TEXT,
|
|
445
|
+
first_seen_at TEXT NOT NULL,
|
|
446
|
+
runs INTEGER NOT NULL DEFAULT 0,
|
|
447
|
+
passed INTEGER NOT NULL DEFAULT 0,
|
|
448
|
+
failed INTEGER NOT NULL DEFAULT 0,
|
|
449
|
+
PRIMARY KEY (repo, sha)
|
|
450
|
+
)`,
|
|
451
|
+
`CREATE TABLE IF NOT EXISTS runs (
|
|
452
|
+
id TEXT PRIMARY KEY,
|
|
453
|
+
repo TEXT NOT NULL,
|
|
454
|
+
sha TEXT NOT NULL,
|
|
455
|
+
eval_id TEXT NOT NULL,
|
|
456
|
+
suite_id TEXT,
|
|
457
|
+
status TEXT NOT NULL,
|
|
458
|
+
score REAL,
|
|
459
|
+
started_at TEXT NOT NULL,
|
|
460
|
+
dirty INTEGER NOT NULL,
|
|
461
|
+
summary_json TEXT NOT NULL,
|
|
462
|
+
uploaded_at TEXT NOT NULL
|
|
463
|
+
)`,
|
|
464
|
+
`CREATE INDEX IF NOT EXISTS commits_by_time ON commits (repo, committed_at DESC)`,
|
|
465
|
+
`CREATE INDEX IF NOT EXISTS commits_by_ref ON commits (repo, ref, committed_at DESC)`,
|
|
466
|
+
`CREATE INDEX IF NOT EXISTS runs_by_commit ON runs (repo, sha)`,
|
|
467
|
+
`CREATE INDEX IF NOT EXISTS runs_by_eval ON runs (repo, eval_id, started_at)`
|
|
468
|
+
];
|
|
469
|
+
/** Newest first; commits without a commit time order by first upload. */
|
|
470
|
+
const BY_TIME = `ORDER BY COALESCE(committed_at, first_seen_at) DESC, first_seen_at DESC`;
|
|
471
|
+
var ShareIndexDb = class {
|
|
472
|
+
sql;
|
|
473
|
+
now;
|
|
474
|
+
constructor(sql, now = () => /* @__PURE__ */ new Date()) {
|
|
475
|
+
this.sql = sql;
|
|
476
|
+
this.now = now;
|
|
477
|
+
for (const statement of MIGRATIONS) sql.run(statement);
|
|
478
|
+
}
|
|
479
|
+
hasRun(runId) {
|
|
480
|
+
return this.sql.all("SELECT 1 FROM runs WHERE id = ?", runId).length > 0;
|
|
481
|
+
}
|
|
482
|
+
/** Idempotent: recording an indexed run again changes nothing. */
|
|
483
|
+
recordRun(run) {
|
|
484
|
+
const at = this.now().toISOString();
|
|
485
|
+
this.sql.transaction(() => {
|
|
486
|
+
if (this.hasRun(run.id)) return;
|
|
487
|
+
this.sql.run(`INSERT INTO repos (key, url, project, first_seen_at, last_upload_at)
|
|
488
|
+
VALUES (?, ?, ?, ?, ?)
|
|
489
|
+
ON CONFLICT (key) DO UPDATE SET last_upload_at = excluded.last_upload_at`, run.repoKey, run.source.repo, run.source.project, at, at);
|
|
490
|
+
this.sql.run(`INSERT INTO commits (repo, sha, ref, committed_at, first_seen_at)
|
|
491
|
+
VALUES (?, ?, ?, ?, ?)
|
|
492
|
+
ON CONFLICT (repo, sha) DO UPDATE SET
|
|
493
|
+
ref = COALESCE(commits.ref, excluded.ref),
|
|
494
|
+
committed_at = COALESCE(commits.committed_at, excluded.committed_at)`, run.repoKey, run.source.commit, run.source.ref ?? null, run.source.committedAt ?? null, at);
|
|
495
|
+
this.sql.run(`INSERT INTO runs (id, repo, sha, eval_id, suite_id, status, score,
|
|
496
|
+
started_at, dirty, summary_json, uploaded_at)
|
|
497
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, run.id, run.repoKey, run.source.commit, run.evalId, run.suiteId ?? null, run.status, run.score ?? null, run.startedAt, run.source.dirty ? 1 : 0, JSON.stringify(run.summary), at);
|
|
498
|
+
if (!run.source.dirty) this.sql.run(`UPDATE commits SET runs = runs + 1,
|
|
499
|
+
passed = passed + ?, failed = failed + ?
|
|
500
|
+
WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0, run.status === "failed" || run.status === "errored" ? 1 : 0, run.repoKey, run.source.commit);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
repos() {
|
|
504
|
+
return this.sql.all(`SELECT key, url, project, first_seen_at AS firstSeenAt,
|
|
505
|
+
last_upload_at AS lastUploadAt,
|
|
506
|
+
(SELECT COUNT(*) FROM commits WHERE commits.repo = repos.key) AS commits
|
|
507
|
+
FROM repos ORDER BY last_upload_at DESC`);
|
|
508
|
+
}
|
|
509
|
+
commits(repo) {
|
|
510
|
+
return this.sql.all(`SELECT sha AS "commit", ref, committed_at AS committedAt,
|
|
511
|
+
first_seen_at AS firstSeenAt, runs, passed, failed
|
|
512
|
+
FROM commits WHERE repo = ? ${BY_TIME}`, repo).map(({ ref, committedAt, ...row }) => ({
|
|
513
|
+
...row,
|
|
514
|
+
...ref ? { ref } : {},
|
|
515
|
+
...committedAt ? { committedAt } : {}
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
runs(repo, sha) {
|
|
519
|
+
return this.sql.all("SELECT summary_json FROM runs WHERE repo = ? AND sha = ? ORDER BY started_at DESC", repo, sha).map((row) => JSON.parse(row.summary_json));
|
|
520
|
+
}
|
|
521
|
+
resolve(repo, ref) {
|
|
522
|
+
if (/^[0-9a-f]{7,40}$/.test(ref)) {
|
|
523
|
+
const matches = this.sql.all("SELECT sha FROM commits WHERE repo = ? AND sha LIKE ? LIMIT 2", repo, `${ref}%`);
|
|
524
|
+
if (matches.length === 1) return matches[0].sha;
|
|
525
|
+
if (matches.length > 1) return void 0;
|
|
526
|
+
}
|
|
527
|
+
return this.sql.all(`SELECT sha FROM commits WHERE repo = ? AND ref = ? ${BY_TIME} LIMIT 1`, repo, ref)[0]?.sha;
|
|
528
|
+
}
|
|
529
|
+
latest(repo) {
|
|
530
|
+
return this.sql.all(`SELECT sha FROM commits WHERE repo = ? ${BY_TIME} LIMIT 1`, repo)[0]?.sha;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
/** Adapt the synchronous index (bun:sqlite) to the app's async interface. */
|
|
534
|
+
function asyncIndex(db) {
|
|
535
|
+
return {
|
|
536
|
+
hasRun: async (runId) => db.hasRun(runId),
|
|
537
|
+
recordRun: async (run) => db.recordRun(run),
|
|
538
|
+
repos: async () => db.repos(),
|
|
539
|
+
commits: async (repo) => db.commits(repo),
|
|
540
|
+
runs: async (repo, sha) => db.runs(repo, sha),
|
|
541
|
+
resolve: async (repo, ref) => db.resolve(repo, ref),
|
|
542
|
+
latest: async (repo) => db.latest(repo)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
export { allowedRunPath as a, dashboardIndex as c, encodeBundle as d, parseUpload as i, BUNDLE_FORMAT as l, asyncIndex as n, repoKey as o, createShareApp as r, validRepoKey as s, ShareIndexDb as t, decodeBundle as u };
|
|
547
|
+
|
|
548
|
+
//# sourceMappingURL=index-db-vbqEGFaU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-db-vbqEGFaU.mjs","names":[],"sources":["../../share/src/bundle.ts","../../share/src/html.ts","../../share/src/paths.ts","../../share/src/upload.ts","../../share/src/app.ts","../../share/src/index-db.ts"],"sourcesContent":["/**\n * A run bundle: the snapshot files for one run, as one gzip-compressed body.\n *\n * \"EKBUNDLE\" (8 bytes) · header length (u32 BE) · header JSON · file bytes…\n * header = { format: 1, files: [{ path, size }, …] }, bytes in that order.\n *\n * Both ends are EvalKit, so this is simpler than tar and has no path-length limit.\n */\nexport const BUNDLE_FORMAT = 1;\nconst MAGIC = new TextEncoder().encode('EKBUNDLE');\n\nexport type BundleFile = { path: string; data: Uint8Array };\n\nasync function pipe(\n data: Uint8Array,\n transform: CompressionStream | DecompressionStream,\n): Promise<Uint8Array> {\n const stream = new Blob([data as BlobPart]).stream().pipeThrough(transform);\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\nexport async function encodeBundle(files: BundleFile[]): Promise<Uint8Array> {\n const header = new TextEncoder().encode(\n JSON.stringify({\n format: BUNDLE_FORMAT,\n files: files.map(({ path, data }) => ({ path, size: data.byteLength })),\n }),\n );\n const total =\n MAGIC.length +\n 4 +\n header.length +\n files.reduce((sum, file) => sum + file.data.byteLength, 0);\n const raw = new Uint8Array(total);\n raw.set(MAGIC, 0);\n new DataView(raw.buffer).setUint32(MAGIC.length, header.length);\n raw.set(header, MAGIC.length + 4);\n let offset = MAGIC.length + 4 + header.length;\n for (const file of files) {\n raw.set(file.data, offset);\n offset += file.data.byteLength;\n }\n return pipe(raw, new CompressionStream('gzip'));\n}\n\nexport class BundleError extends Error {}\n\n/** Decode and check the framing; path rules are enforced by the caller. */\nexport async function decodeBundle(\n body: Uint8Array,\n limits: { maxBytes: number },\n): Promise<BundleFile[]> {\n let raw: Uint8Array;\n try {\n raw = await pipe(body, new DecompressionStream('gzip'));\n } catch {\n throw new BundleError('Body is not a gzip-compressed EvalKit bundle');\n }\n if (raw.byteLength > limits.maxBytes)\n throw new BundleError(`Bundle exceeds ${limits.maxBytes} bytes`);\n if (\n raw.byteLength < MAGIC.length + 4 ||\n MAGIC.some((byte, index) => raw[index] !== byte)\n )\n throw new BundleError('Not an EvalKit bundle');\n const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);\n const headerLength = view.getUint32(MAGIC.length);\n const start = MAGIC.length + 4;\n let header: { format?: unknown; files?: { path: unknown; size: unknown }[] };\n try {\n header = JSON.parse(\n new TextDecoder().decode(raw.subarray(start, start + headerLength)),\n );\n } catch {\n throw new BundleError('Bundle header is not valid JSON');\n }\n if (header.format !== BUNDLE_FORMAT)\n throw new BundleError(`Unsupported bundle format ${String(header.format)}`);\n if (!Array.isArray(header.files))\n throw new BundleError('Bundle header has no file list');\n let offset = start + headerLength;\n const files: BundleFile[] = [];\n for (const entry of header.files) {\n if (\n typeof entry.path !== 'string' ||\n typeof entry.size !== 'number' ||\n !Number.isSafeInteger(entry.size) ||\n entry.size < 0 ||\n offset + entry.size > raw.byteLength\n )\n throw new BundleError('Bundle file list does not match its contents');\n files.push({\n path: entry.path,\n data: raw.subarray(offset, offset + entry.size),\n });\n offset += entry.size;\n }\n if (offset !== raw.byteLength)\n throw new BundleError('Bundle has trailing bytes');\n return files;\n}\n","/**\n * Turn the dashboard's index.html into a read-only page. The app derives its\n * router basename from `<base href>`. A static snapshot reads `data/…` below\n * the base; on the share server, `share` says where the API is.\n */\nexport type ShareContext = {\n /** All repositories: `/api/v1/repos`. */\n repos: string;\n /** This repository's commits, for the version picker. */\n commits?: string;\n /** This commit's snapshot data (replaces `<base>/data/`). */\n data?: string;\n repo?: string;\n /** A ref or SHA with no shared results. */\n missing?: string;\n};\n\nexport function dashboardIndex(\n html: string,\n options: { base: string; assets?: string; share?: ShareContext },\n): string {\n const attrs = [\n 'data-evalkit-snapshot=\"static\"',\n ...(options.share\n ? [`data-evalkit-share=\"${escapeAttr(JSON.stringify(options.share))}\"`]\n : []),\n ].join(' ');\n const result = html\n .replace('<html lang=\"en\">', `<html lang=\"en\" ${attrs}>`)\n .replace(\n '<head>',\n `<head>\\n <base href=\"${escapeAttr(options.base)}\" />`,\n )\n .replaceAll('\"/assets/', `\"${options.assets ?? './assets/'}`);\n if (\n !result.includes('data-evalkit-snapshot=\"static\"') ||\n !result.includes('<base href=')\n )\n throw new Error('Bundled dashboard index.html has an unexpected shape');\n return result;\n}\n\nfunction escapeAttr(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('\"', '"')\n .replaceAll('<', '<');\n}\n","const UUID =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst TRIAL_JSON =\n /^trials\\/([^/]+)\\/(detail|events|artifacts|workspace)\\.json$/;\nconst TRIAL_FILE = /^trials\\/([^/]+)\\/workspace\\/(.+)$/;\n\nexport const isUuid = (value: string) => UUID.test(value);\nexport const isSha = (value: string) => /^[0-9a-f]{40}$/.test(value);\n\n/** One path segment that cannot escape or alias another location. */\nexport function safeSegment(segment: string): boolean {\n return (\n segment.length > 0 &&\n segment.length <= 255 &&\n segment !== '.' &&\n segment !== '..' &&\n !/[\\\\/\\0]/.test(segment)\n );\n}\n\n/**\n * The only files a run bundle may contain (the snapshot format from\n * `evalkit build`, for one run). Never evaluator workspaces.\n */\nexport function allowedRunPath(path: string): boolean {\n if (path === 'run.json' || path === 'catalog.json' || path === 'trials.json')\n return true;\n const json = TRIAL_JSON.exec(path);\n if (json) return isUuid(json[1]!);\n const file = TRIAL_FILE.exec(path);\n return Boolean(\n file && isUuid(file[1]!) && file[2]!.split('/').every(safeSegment),\n );\n}\n\n/**\n * Repository key: normalized host/path, plus `/~<project>` for an eval\n * project below the repository root. Used in storage keys and URLs.\n */\nexport function repoKey(source: { repo: string; project: string }): string {\n return source.project ? `${source.repo}/~${source.project}` : source.repo;\n}\n\nexport function validRepoKey(key: string): boolean {\n const segments = key.split('/');\n return (\n segments.length >= 2 &&\n !segments.includes('-') &&\n segments.every(safeSegment)\n );\n}\n","import type { BundleFile } from './bundle.js';\nimport {\n allowedRunPath,\n isSha,\n isUuid,\n repoKey,\n validRepoKey,\n} from './paths.js';\n\n/** Rejected input: the client sent something the server will not store. */\nexport class Rejected extends Error {}\n\n/** Finished runs only: a shared run's files are immutable once stored. */\nexport type SharedRunStatus = 'passed' | 'failed' | 'errored';\n\nexport type SharedSource = {\n repo: string;\n project: string;\n commit: string;\n ref?: string;\n committedAt?: string;\n dirty: boolean;\n};\n\n/** The fields of an uploaded run.json the server relies on. */\nexport type SharedRun = {\n id: string;\n evalId: string;\n suiteId?: string;\n status: SharedRunStatus;\n score?: number;\n startedAt: string;\n source: SharedSource;\n /** Repository key (`repo` plus `/~project`), checked to be URL- and path-safe. */\n repoKey: string;\n /** The whole run.json object, served as-is in runs.json. */\n summary: unknown;\n};\n\nexport type RunCatalog = {\n catalog: unknown[];\n evals: unknown[];\n suites: unknown[];\n};\n\n/** A run bundle that passed every check; storing it cannot fail on its contents. */\nexport type RunUpload = {\n run: SharedRun;\n runJson: Uint8Array;\n catalog?: Uint8Array;\n /** trials.json and trials/** files. */\n files: BundleFile[];\n};\n\nexport function parseUpload(bundle: readonly BundleFile[]): RunUpload {\n const byPath = new Map<string, Uint8Array>();\n for (const file of bundle) {\n if (!allowedRunPath(file.path))\n throw new Rejected(`File not allowed in a run bundle: ${file.path}`);\n if (byPath.has(file.path))\n throw new Rejected(`Duplicate file in bundle: ${file.path}`);\n byPath.set(file.path, file.data);\n }\n const runJson = byPath.get('run.json');\n if (!runJson) throw new Rejected('Bundle has no run.json');\n const catalog = byPath.get('catalog.json');\n if (catalog) parseCatalog(catalog);\n return {\n run: parseRun(runJson),\n runJson,\n ...(catalog ? { catalog } : {}),\n files: bundle.filter(\n (file) => file.path !== 'run.json' && file.path !== 'catalog.json',\n ),\n };\n}\n\nexport function parseRun(data: Uint8Array): SharedRun {\n const value = record(parseJson(data, 'run.json'), 'run.json');\n const source = record(value.source, 'run.json source');\n const id = text(value.id, 'id');\n if (!isUuid(id)) throw new Rejected('run.json id must be a UUID');\n const commit = text(source.commit, 'source.commit');\n if (!isSha(commit))\n throw new Rejected('source.commit must be a full 40-character SHA');\n if (typeof source.dirty !== 'boolean')\n throw new Rejected('source.dirty must be a boolean');\n const status = value.status;\n if (status !== 'passed' && status !== 'failed' && status !== 'errored')\n throw new Rejected(\n `Only finished runs can be shared (status ${JSON.stringify(status)})`,\n );\n if (value.score !== undefined && typeof value.score !== 'number')\n throw new Rejected('score must be a number');\n const shared: SharedSource = {\n repo: text(source.repo, 'source.repo'),\n project: projectPath(source.project),\n commit,\n dirty: source.dirty,\n ...optionalText(source.ref, 'ref', 'source.ref'),\n ...optionalText(source.committedAt, 'committedAt', 'source.committedAt'),\n };\n const key = repoKey(shared);\n if (!validRepoKey(key)) throw new Rejected(`Invalid repository: ${key}`);\n return {\n id,\n evalId: text(value.evalId, 'evalId'),\n status,\n startedAt: text(value.startedAt, 'startedAt'),\n source: shared,\n repoKey: key,\n summary: value,\n ...optionalText(value.suiteId, 'suiteId', 'suiteId'),\n ...(typeof value.score === 'number' ? { score: value.score } : {}),\n };\n}\n\nexport function parseCatalog(data: Uint8Array): RunCatalog {\n const value = record(parseJson(data, 'catalog.json'), 'catalog.json');\n const list = (field: keyof RunCatalog) => {\n const items = value[field];\n if (!Array.isArray(items))\n throw new Rejected(`catalog.json ${field} must be a list`);\n return items as unknown[];\n };\n return {\n catalog: list('catalog'),\n evals: list('evals'),\n suites: list('suites'),\n };\n}\n\n/** The eval project's path in the repository; `''` at the root. */\nfunction projectPath(value: unknown): string {\n if (typeof value !== 'string')\n throw new Rejected('source.project must be a string');\n return value;\n}\n\nfunction parseJson(data: Uint8Array, name: string): unknown {\n try {\n return JSON.parse(new TextDecoder().decode(data));\n } catch {\n throw new Rejected(`${name} is not valid JSON`);\n }\n}\n\nfunction record(value: unknown, name: string): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value))\n throw new Rejected(`${name} must be an object`);\n return value as Record<string, unknown>;\n}\n\nfunction text(value: unknown, name: string): string {\n if (typeof value !== 'string' || !value)\n throw new Rejected(`${name} must be a non-empty string`);\n return value;\n}\n\nfunction optionalText<K extends string>(\n value: unknown,\n key: K,\n name: string,\n): Partial<Record<K, string>> {\n if (value === undefined || value === null || value === '') return {};\n return { [key]: text(value, name) } as Record<K, string>;\n}\n","import { Hono, type Context } from 'hono';\nimport { BundleError, decodeBundle } from './bundle.js';\nimport type { ShareFiles } from './files.js';\nimport { dashboardIndex } from './html.js';\nimport type { ShareIndex } from './index-db.js';\nimport { isSha, isUuid, safeSegment, validRepoKey } from './paths.js';\nimport {\n parseCatalog,\n parseRun,\n parseUpload,\n Rejected,\n type RunCatalog,\n type RunUpload,\n type SharedRun,\n} from './upload.js';\n\n/** The bundled dashboard: its index.html template and its JS/CSS files. */\nexport type DashboardAssets = {\n index(): Promise<string>;\n get(path: string): Promise<Response | null>;\n};\n\n/** One structured log event. Bounded fields only; never file contents. */\nexport type ShareEvent =\n | {\n event: 'share.upload';\n outcome: 'created' | 'exists';\n repo: string;\n commit: string;\n runId: string;\n files: number;\n bytes: number;\n }\n | { event: 'share.upload'; outcome: 'rejected'; reason: string }\n | { event: 'share.reindex'; runs: number; indexed: number }\n | { event: 'share.error'; method: string; path: string; error: string };\n\nexport type ShareAppOptions = {\n files: ShareFiles;\n index: ShareIndex;\n assets: DashboardAssets;\n /** Uncompressed bundle limit; the compressed body must also fit a Worker request. */\n maxBundleBytes?: number;\n /** Defaults to one JSON line per event on stdout (Workers Logs indexes these). */\n log?: (event: ShareEvent) => void;\n};\n\n/** What a non-API path shows. */\ntype SharePage =\n | { kind: 'repositories' }\n | { kind: 'missing'; repo: string; ref?: string }\n | { kind: 'commit'; repo: string; sha: string };\n\nconst IMMUTABLE = 'public, max-age=31536000, immutable';\nconst NO_CACHE = { 'cache-control': 'no-cache' };\nconst RUN_FILE =\n /^repos\\/(.+)\\/commits\\/([0-9a-f]{40})\\/runs\\/([^/]+)\\/run\\.json$/;\nconst EMPTY_CATALOG: RunCatalog = { catalog: [], evals: [], suites: [] };\n\nconst jsonLine = (event: ShareEvent) => console.log(JSON.stringify(event));\n\n/**\n * The share server: a small JSON API plus the dashboard app. It stores\n * uploaded run bundles and shows one read-only snapshot per repository commit.\n *\n * POST /api/v1/uploads one run bundle\n * POST /api/v1/reindex rebuild the index from stored run.json files\n * GET /api/v1/repos indexed repositories\n * GET /api/v1/repos/<repo>/-/commits commits, newest first\n * GET /api/v1/repos/<repo>/-/<sha>/data/… snapshot data\n * GET /assets/* dashboard JS/CSS\n * GET /<repo> → latest commit\n * GET /<repo>/-/<ref|prefix>/… → full SHA\n * GET everything else the dashboard app\n *\n * There is no authentication: run it on a private network.\n */\nexport function createShareApp(options: ShareAppOptions) {\n const { files, index, assets } = options;\n const log = options.log ?? jsonLine;\n const maxBundleBytes = options.maxBundleBytes ?? 256 * 1024 * 1024;\n const app = new Hono();\n\n app.onError((error, context) => {\n log({\n event: 'share.error',\n method: context.req.method,\n path: context.req.path,\n error: error instanceof Error ? error.message : String(error),\n });\n return context.json({ error: 'Internal error' }, 500);\n });\n\n app.post('/api/v1/uploads', async (context) => {\n let upload: RunUpload;\n try {\n const body = new Uint8Array(await context.req.arrayBuffer());\n upload = parseUpload(\n await decodeBundle(body, { maxBytes: maxBundleBytes }),\n );\n } catch (error) {\n if (!(error instanceof BundleError || error instanceof Rejected))\n throw error;\n log({\n event: 'share.upload',\n outcome: 'rejected',\n reason: error.message,\n });\n return context.json({ error: error.message }, 400);\n }\n const { run } = upload;\n const url = new URL(\n `/${run.repoKey}/-/${run.source.commit}/runs/${run.id}`,\n context.req.url,\n ).href;\n const exists = await index.hasRun(run.id);\n if (!exists) await storeRun(upload);\n const outcome = exists ? 'exists' : 'created';\n log({\n event: 'share.upload',\n outcome,\n repo: run.repoKey,\n commit: run.source.commit,\n runId: run.id,\n files: upload.files.length,\n bytes: upload.files.reduce((sum, file) => sum + file.data.byteLength, 0),\n });\n return context.json(\n { status: outcome, url },\n outcome === 'created' ? 201 : 200,\n );\n });\n\n /** Files first, index last: a run becomes visible only once it is indexed. */\n async function storeRun({\n run,\n runJson,\n catalog,\n files: runFiles,\n }: RunUpload) {\n const commitBase = `repos/${run.repoKey}/commits/${run.source.commit}`;\n const runBase = `${commitBase}/runs/${run.id}`;\n for (const file of runFiles)\n await files.put(`${runBase}/${file.path}`, file.data);\n // A committed run's catalog describes the commit; a dirty run's only fills a gap.\n if (\n catalog &&\n (!run.source.dirty || !(await files.get(`${commitBase}/catalog.json`)))\n )\n await files.put(`${commitBase}/catalog.json`, catalog);\n await files.put(`${runBase}/run.json`, runJson);\n await index.recordRun(run);\n }\n\n app.post('/api/v1/reindex', async (context) => {\n let runs = 0;\n let indexed = 0;\n for await (const key of files.list('repos/')) {\n if (!RUN_FILE.test(key)) continue;\n runs += 1;\n const run = storedRun(await readBytes(await files.get(key)));\n if (!run || (await index.hasRun(run.id))) continue;\n await index.recordRun(run);\n indexed += 1;\n }\n log({ event: 'share.reindex', runs, indexed });\n return context.json({ runs, indexed });\n });\n\n app.get('/api/v1/repos', async (context) =>\n context.json({ repos: await index.repos() }, 200, NO_CACHE),\n );\n\n // /api/v1/repos/<repo>/-/commits and /api/v1/repos/<repo>/-/<sha>/data/…\n app.get('/api/v1/repos/*', async (context) => {\n const path = context.req.path.slice('/api/v1/repos/'.length);\n const marker = path.indexOf('/-/');\n const key = marker === -1 ? '' : path.slice(0, marker);\n if (!validRepoKey(key))\n return context.json({ error: 'Unknown repository' }, 404);\n const [sha = '', kind, ...segments] = path.slice(marker + 3).split('/');\n if (sha === 'commits' && kind === undefined)\n return context.json(\n { repo: key, commits: await index.commits(key) },\n 200,\n NO_CACHE,\n );\n if (!isSha(sha) || kind !== 'data')\n return context.json({ error: 'Not found' }, 404);\n return serveData(context, key, sha, segments.map(decodeURIComponent));\n });\n\n // The bundled dashboard's JS/CSS. A Worker serves these as static assets\n // before this app runs; the Bun server serves them here.\n app.get('/assets/*', async (context) => {\n const path = context.req.path.slice('/assets/'.length);\n if (!path.split('/').every(safeSegment)) return context.notFound();\n const response = await assets.get(path);\n if (!response) return context.notFound();\n const headers = new Headers(response.headers);\n headers.set('cache-control', IMMUTABLE);\n return new Response(response.body, { status: response.status, headers });\n });\n\n app.all('/api/*', (context) => context.json({ error: 'Not found' }, 404));\n\n // Every other path is the dashboard app. Short refs resolve to full SHAs\n // first, so every commit page URL names one immutable commit.\n app.get('*', async (context) => {\n const url = new URL(context.req.url);\n const path = url.pathname.replace(/\\/+$/, '');\n const marker = path.indexOf('/-/');\n if (marker === -1) return repositoryPage(context, path.slice(1));\n const repo = path.slice(1, marker);\n const [ref = '', ...rest] = url.pathname.slice(marker + 3).split('/');\n const sha = validRepoKey(repo)\n ? await index.resolve(repo, decodeURIComponent(ref))\n : undefined;\n if (!sha) return dashboardPage(context, { kind: 'missing', repo, ref });\n if (sha !== ref)\n return context.redirect(\n `/${repo}/-/${sha}/${rest.join('/')}${url.search}`,\n );\n return dashboardPage(context, { kind: 'commit', repo, sha });\n });\n\n /** `/` lists repositories; `/<repo>` opens its latest commit. */\n async function repositoryPage(context: Context, repo: string) {\n if (!repo) return dashboardPage(context, { kind: 'repositories' });\n const latest = validRepoKey(repo) ? await index.latest(repo) : undefined;\n if (latest) return context.redirect(`/${repo}/-/${latest}/`);\n return dashboardPage(context, { kind: 'missing', repo });\n }\n\n /** The dashboard's index.html, told where this page's data lives. */\n async function dashboardPage(context: Context, page: SharePage) {\n const api =\n page.kind === 'repositories'\n ? undefined\n : `/api/v1/repos/${page.repo}/-/`;\n return context.html(\n dashboardIndex(await assets.index(), {\n base: page.kind === 'commit' ? `/${page.repo}/-/${page.sha}/` : '/',\n assets: '/assets/',\n share: {\n repos: '/api/v1/repos',\n ...(page.kind === 'repositories'\n ? {}\n : { repo: page.repo, commits: `${api}commits` }),\n ...(page.kind === 'commit' ? { data: `${api}${page.sha}/` } : {}),\n ...(page.kind === 'missing' && page.ref ? { missing: page.ref } : {}),\n },\n }),\n page.kind === 'missing' ? 404 : 200,\n NO_CACHE,\n );\n }\n\n async function serveData(\n context: Context,\n key: string,\n sha: string,\n segments: string[],\n ) {\n if (!segments.every(safeSegment)) return context.notFound();\n const path = segments.join('/');\n const commitBase = `repos/${key}/commits/${sha}`;\n if (path === 'runs.json')\n return context.json({ runs: await index.runs(key, sha) }, 200, NO_CACHE); // grows as runs are uploaded\n if (['catalog.json', 'evals.json', 'suites.json'].includes(path)) {\n // Parsed at upload, so a stored catalog is always well formed.\n const data = await readBytes(\n await files.get(`${commitBase}/catalog.json`),\n );\n const catalog = data ? parseCatalog(data) : EMPTY_CATALOG;\n return context.json(\n path === 'catalog.json'\n ? { evals: catalog.catalog }\n : path === 'evals.json'\n ? { evals: catalog.evals }\n : { suites: catalog.suites },\n 200,\n NO_CACHE,\n );\n }\n const runFile = /^runs\\/([^/]+)\\/(trials\\.json|trials\\/.+)$/.exec(path);\n if (!runFile || !isUuid(runFile[1]!)) return context.notFound();\n const body = await files.get(`${commitBase}/${path}`);\n if (!body) return context.notFound();\n const json = path.endsWith('.json') && !path.includes('/workspace/');\n return new Response(body as BodyInit, {\n headers: json\n ? { 'content-type': 'application/json', 'cache-control': IMMUTABLE }\n : {\n // Candidate files are untrusted content on this origin: never render them.\n 'content-type': 'application/octet-stream',\n 'x-content-type-options': 'nosniff',\n 'content-security-policy': 'sandbox',\n 'cache-control': IMMUTABLE,\n },\n });\n }\n\n return app;\n}\n\n/** A stored run.json, or undefined if it is not a shareable run. */\nfunction storedRun(data: Uint8Array | null): SharedRun | undefined {\n if (!data) return undefined;\n try {\n return parseRun(data);\n } catch (error) {\n if (error instanceof Rejected) return undefined;\n throw error;\n }\n}\n\nasync function readBytes(\n body: ReadableStream<Uint8Array> | Uint8Array | null,\n): Promise<Uint8Array | null> {\n if (!body) return null;\n if (body instanceof Uint8Array) return body;\n return new Uint8Array(await new Response(body).arrayBuffer());\n}\n","import type { SharedRun } from './upload.js';\n\n/**\n * The share server's metadata index: repositories, commits and runs.\n * R2 (or a directory) holds the files; this index answers \"what exists\" and\n * every listing query. It is derived from stored run.json files and can be\n * rebuilt from them (`POST /api/v1/reindex`).\n */\n\n/** Synchronous SQLite access, provided by Durable Object storage or bun:sqlite. */\nexport type Sql = {\n all<T>(query: string, ...params: SqlValue[]): T[];\n run(query: string, ...params: SqlValue[]): void;\n transaction<T>(work: () => T): T;\n};\nexport type SqlValue = string | number | null;\n\nexport type RepoRow = {\n key: string;\n url: string;\n project: string;\n firstSeenAt: string;\n lastUploadAt: string;\n commits: number;\n};\n\nexport type CommitRow = {\n commit: string;\n ref?: string;\n committedAt?: string;\n firstSeenAt: string;\n runs: number;\n passed: number;\n failed: number;\n};\n\n/** The operations the app needs; async so a Durable Object can sit behind it. */\nexport type ShareIndex = {\n hasRun(runId: string): Promise<boolean>;\n recordRun(run: SharedRun): Promise<void>;\n repos(): Promise<RepoRow[]>;\n commits(repo: string): Promise<CommitRow[]>;\n runs(repo: string, sha: string): Promise<unknown[]>;\n /** Full SHA for a full SHA, a unique prefix (≥ 7), or a ref's latest commit. */\n resolve(repo: string, ref: string): Promise<string | undefined>;\n latest(repo: string): Promise<string | undefined>;\n};\n\nconst MIGRATIONS = [\n `CREATE TABLE IF NOT EXISTS repos (\n key TEXT PRIMARY KEY,\n url TEXT NOT NULL,\n project TEXT NOT NULL,\n first_seen_at TEXT NOT NULL,\n last_upload_at TEXT NOT NULL\n )`,\n `CREATE TABLE IF NOT EXISTS commits (\n repo TEXT NOT NULL,\n sha TEXT NOT NULL,\n ref TEXT,\n committed_at TEXT,\n first_seen_at TEXT NOT NULL,\n runs INTEGER NOT NULL DEFAULT 0,\n passed INTEGER NOT NULL DEFAULT 0,\n failed INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (repo, sha)\n )`,\n `CREATE TABLE IF NOT EXISTS runs (\n id TEXT PRIMARY KEY,\n repo TEXT NOT NULL,\n sha TEXT NOT NULL,\n eval_id TEXT NOT NULL,\n suite_id TEXT,\n status TEXT NOT NULL,\n score REAL,\n started_at TEXT NOT NULL,\n dirty INTEGER NOT NULL,\n summary_json TEXT NOT NULL,\n uploaded_at TEXT NOT NULL\n )`,\n `CREATE INDEX IF NOT EXISTS commits_by_time ON commits (repo, committed_at DESC)`,\n `CREATE INDEX IF NOT EXISTS commits_by_ref ON commits (repo, ref, committed_at DESC)`,\n `CREATE INDEX IF NOT EXISTS runs_by_commit ON runs (repo, sha)`,\n `CREATE INDEX IF NOT EXISTS runs_by_eval ON runs (repo, eval_id, started_at)`,\n];\n\n/** Newest first; commits without a commit time order by first upload. */\nconst BY_TIME = `ORDER BY COALESCE(committed_at, first_seen_at) DESC, first_seen_at DESC`;\n\nexport class ShareIndexDb {\n constructor(\n private readonly sql: Sql,\n private readonly now: () => Date = () => new Date(),\n ) {\n for (const statement of MIGRATIONS) sql.run(statement);\n }\n\n hasRun(runId: string): boolean {\n return this.sql.all('SELECT 1 FROM runs WHERE id = ?', runId).length > 0;\n }\n\n /** Idempotent: recording an indexed run again changes nothing. */\n recordRun(run: SharedRun): void {\n const at = this.now().toISOString();\n this.sql.transaction(() => {\n if (this.hasRun(run.id)) return;\n this.sql.run(\n `INSERT INTO repos (key, url, project, first_seen_at, last_upload_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (key) DO UPDATE SET last_upload_at = excluded.last_upload_at`,\n run.repoKey,\n run.source.repo,\n run.source.project,\n at,\n at,\n );\n this.sql.run(\n `INSERT INTO commits (repo, sha, ref, committed_at, first_seen_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (repo, sha) DO UPDATE SET\n ref = COALESCE(commits.ref, excluded.ref),\n committed_at = COALESCE(commits.committed_at, excluded.committed_at)`,\n run.repoKey,\n run.source.commit,\n run.source.ref ?? null,\n run.source.committedAt ?? null,\n at,\n );\n this.sql.run(\n `INSERT INTO runs (id, repo, sha, eval_id, suite_id, status, score,\n started_at, dirty, summary_json, uploaded_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n run.id,\n run.repoKey,\n run.source.commit,\n run.evalId,\n run.suiteId ?? null,\n run.status,\n run.score ?? null,\n run.startedAt,\n run.source.dirty ? 1 : 0,\n JSON.stringify(run.summary),\n at,\n );\n // Dirty runs are shown but never count towards a commit's results.\n if (!run.source.dirty)\n this.sql.run(\n `UPDATE commits SET runs = runs + 1,\n passed = passed + ?, failed = failed + ?\n WHERE repo = ? AND sha = ?`,\n run.status === 'passed' ? 1 : 0,\n run.status === 'failed' || run.status === 'errored' ? 1 : 0,\n run.repoKey,\n run.source.commit,\n );\n });\n }\n\n repos(): RepoRow[] {\n return this.sql.all<RepoRow>(\n `SELECT key, url, project, first_seen_at AS firstSeenAt,\n last_upload_at AS lastUploadAt,\n (SELECT COUNT(*) FROM commits WHERE commits.repo = repos.key) AS commits\n FROM repos ORDER BY last_upload_at DESC`,\n );\n }\n\n commits(repo: string): CommitRow[] {\n return this.sql\n .all<CommitRow & { ref: string | null; committedAt: string | null }>(\n `SELECT sha AS \"commit\", ref, committed_at AS committedAt,\n first_seen_at AS firstSeenAt, runs, passed, failed\n FROM commits WHERE repo = ? ${BY_TIME}`,\n repo,\n )\n .map(({ ref, committedAt, ...row }) => ({\n ...row,\n ...(ref ? { ref } : {}),\n ...(committedAt ? { committedAt } : {}),\n }));\n }\n\n runs(repo: string, sha: string): unknown[] {\n return this.sql\n .all<{ summary_json: string }>(\n 'SELECT summary_json FROM runs WHERE repo = ? AND sha = ? ORDER BY started_at DESC',\n repo,\n sha,\n )\n .map((row) => JSON.parse(row.summary_json) as unknown);\n }\n\n resolve(repo: string, ref: string): string | undefined {\n if (/^[0-9a-f]{7,40}$/.test(ref)) {\n const matches = this.sql.all<{ sha: string }>(\n 'SELECT sha FROM commits WHERE repo = ? AND sha LIKE ? LIMIT 2',\n repo,\n `${ref}%`,\n );\n if (matches.length === 1) return matches[0]!.sha;\n if (matches.length > 1) return undefined; // ambiguous prefix\n }\n return this.sql.all<{ sha: string }>(\n `SELECT sha FROM commits WHERE repo = ? AND ref = ? ${BY_TIME} LIMIT 1`,\n repo,\n ref,\n )[0]?.sha;\n }\n\n latest(repo: string): string | undefined {\n return this.sql.all<{ sha: string }>(\n `SELECT sha FROM commits WHERE repo = ? ${BY_TIME} LIMIT 1`,\n repo,\n )[0]?.sha;\n }\n}\n\n/** Adapt the synchronous index (bun:sqlite) to the app's async interface. */\nexport function asyncIndex(db: ShareIndexDb): ShareIndex {\n return {\n hasRun: async (runId) => db.hasRun(runId),\n recordRun: async (run) => db.recordRun(run),\n repos: async () => db.repos(),\n commits: async (repo) => db.commits(repo),\n runs: async (repo, sha) => db.runs(repo, sha),\n resolve: async (repo, ref) => db.resolve(repo, ref),\n latest: async (repo) => db.latest(repo),\n };\n}\n"],"mappings":"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"}
|