@leostera/evalkit 0.9.0 → 0.11.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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>EvalKit</title>
7
- <script type="module" crossorigin src="/assets/index-By1SJ1on.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-geXMXCx9.css">
7
+ <script type="module" crossorigin src="/assets/index-DgoX2x_M.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CFJzcw3o.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -1,4 +1,5 @@
1
- import { a as upgradeManifest, i as upgradeEvent, o as upgradeStatus, r as upgradeCatalog } from "./upgrade-60SIRius.mjs";
1
+ import { a as upgradeManifest, i as upgradeEvent, o as upgradeStatus, r as upgradeCatalog } from "./upgrade-rMsGzUEJ.mjs";
2
+ import { n as upgradeScoring, t as convertTrajectory } from "./upgrade-transcript-C8DUykLd.mjs";
2
3
  import { Hono } from "hono";
3
4
  //#region ../share/src/bundle.ts
4
5
  /**
@@ -82,40 +83,40 @@ function escapeAttr(value) {
82
83
  }
83
84
  //#endregion
84
85
  //#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);
86
+ const decoder$1 = new TextDecoder();
87
+ const encoder$1 = new TextEncoder();
88
+ const parse$1 = (data) => data ? JSON.parse(decoder$1.decode(data)) : void 0;
89
+ const encode$1 = (value) => encoder$1.encode(`${JSON.stringify(value)}\n`);
90
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
90
91
  /** True for a version 1 run.json: no `snapshotVersion`. */
91
92
  function isLegacyRun(runJson) {
92
- return isRecord(runJson) && runJson.snapshotVersion === void 0;
93
+ return isRecord$1(runJson) && runJson.snapshotVersion === void 0;
93
94
  }
94
95
  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);
96
+ const run = parse$1(files.get("run.json"));
97
+ if (!isRecord$1(run)) throw new Error("run.json must be an object");
98
+ const listed = parse$1(files.get("trials.json"));
99
+ const trials = (isRecord$1(listed) && Array.isArray(listed.trials) ? listed.trials : []).filter(isRecord$1);
99
100
  const size = trials.length;
100
101
  return trials.map((trial) => {
101
102
  const id = size === 1 ? String(run.id) : String(trial.id);
102
103
  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 : {};
104
+ const detail = parse$1(files.get(`${prefix}detail.json`));
105
+ const trialManifest = isRecord$1(detail) && isRecord$1(detail.manifest) ? detail.manifest : {};
106
+ const trialSummary = isRecord$1(detail) && isRecord$1(detail.summary) ? detail.summary : {};
106
107
  const index = typeof trial.index === "number" ? trial.index : trials.indexOf(trial);
107
108
  const batch = size > 1 ? {
108
109
  id: String(run.id),
109
110
  index,
110
111
  size
111
112
  } : 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) : [];
113
+ const scores = Array.isArray(trial.scores) ? trial.scores.filter(isRecord$1) : [];
114
+ const metrics = Array.isArray(trial.metrics) ? trial.metrics.filter(isRecord$1) : [];
115
+ const datasets = Array.isArray(trialManifest.datasets) ? trialManifest.datasets.filter(isRecord$1) : [];
115
116
  const status = String(trial.status);
116
117
  const pick = (keys) => Object.fromEntries(keys.flatMap((key) => run[key] === void 0 ? [] : [[key, run[key]]]));
117
118
  const out = /* @__PURE__ */ new Map();
118
- out.set("run.json", encode({
119
+ out.set("run.json", encode$1({
119
120
  snapshotVersion: 2,
120
121
  id,
121
122
  ...pick([
@@ -153,7 +154,7 @@ function upgradeSnapshotRun(files) {
153
154
  })) } : {}
154
155
  }));
155
156
  const passed = trialSummary.scoring?.passed;
156
- out.set("detail.json", encode({
157
+ out.set("detail.json", encode$1({
157
158
  scores,
158
159
  ...metrics.length ? { metrics } : {},
159
160
  checkpoints: Array.isArray(trial.checkpoints) ? trial.checkpoints : [],
@@ -181,8 +182,8 @@ function upgradeSnapshotRun(files) {
181
182
  const rest = path.slice(prefix.length);
182
183
  if (rest === "detail.json") continue;
183
184
  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) : [] }));
185
+ const events = parse$1(data);
186
+ out.set(rest, encode$1({ events: isRecord$1(events) && Array.isArray(events.events) ? events.events.map(upgradeEvent) : [] }));
186
187
  } else out.set(rest, data);
187
188
  }
188
189
  return {
@@ -193,12 +194,107 @@ function upgradeSnapshotRun(files) {
193
194
  }
194
195
  /** A stored or uploaded version 1 catalog, as version 2 stores it. */
195
196
  function upgradeCatalogFile(data) {
196
- return encode(upgradeCatalog(parse(data)));
197
+ return encode$1(upgradeCatalog(parse$1(data)));
198
+ }
199
+ //#endregion
200
+ //#region ../share/src/transcripts.ts
201
+ /**
202
+ * Converts one run in snapshot format version 2 (EvalKit 0.9 and 0.10) to
203
+ * version 3 (RFD0009): `events.json`, its trajectory, becomes
204
+ * `transcript.json`, an ATXF transcript with EvalKit's events placed in it;
205
+ * `detail.json` points into it, and token metrics no longer count cache
206
+ * reads, in `detail.json` and `run.json` alike. Pure: the Worker runs it on
207
+ * upload, in `POST /api/v1/upgrade`, and on read for runs not upgraded yet,
208
+ * so all three give the same files.
209
+ *
210
+ * Throws when the run cannot be converted, such as tokens that do not add
211
+ * up; the caller keeps the run as it was.
212
+ */
213
+ function upgradeV2Run(files) {
214
+ const run = record$1(parse(files.get("run.json")), "run.json");
215
+ const detail = record$1(parse(files.get("detail.json")) ?? {}, "detail.json");
216
+ const events = parse(files.get("events.json"));
217
+ const trajectory = isRecord(events) && Array.isArray(events.events) ? events.events : [];
218
+ const manifest = isRecord(detail.manifest) ? detail.manifest : {};
219
+ const runId = String(run.id);
220
+ const startedAt = typeof run.startedAt === "string" ? run.startedAt : (/* @__PURE__ */ new Date(0)).toISOString();
221
+ const aut = isRecord(manifest.aut) ? manifest.aut : void 0;
222
+ const converted = convertTrajectory(trajectory, {
223
+ runId,
224
+ ...typeof run.evalId === "string" ? { evalId: run.evalId } : {},
225
+ startedAt,
226
+ ...aut ? { agent: {
227
+ ...typeof aut.id === "string" ? { id: aut.id } : {},
228
+ ...typeof aut.version === "string" ? { version: aut.version } : {}
229
+ } } : {}
230
+ });
231
+ const upgrade = (scoring) => upgradeScoring(scoring, {
232
+ runId,
233
+ startedAt
234
+ }, converted.recordOfEvent, converted.transcript);
235
+ const scoring = upgrade({
236
+ results: Array.isArray(detail.scores) ? detail.scores : [],
237
+ ...Array.isArray(detail.metrics) ? { metrics: detail.metrics } : {},
238
+ ...Array.isArray(detail.checkpoints) ? { checkpoints: detail.checkpoints } : {},
239
+ passed: false
240
+ });
241
+ const summary = isRecord(detail.summary) ? detail.summary : void 0;
242
+ const upgraded = new Map(files);
243
+ upgraded.delete("events.json");
244
+ upgraded.set("transcript.json", encode({
245
+ transcript: converted.transcript,
246
+ subagents: {},
247
+ runEvents: converted.runEvents
248
+ }));
249
+ upgraded.set("detail.json", encode({
250
+ ...detail,
251
+ scores: scoring.results,
252
+ ...scoring.metrics ? { metrics: scoring.metrics } : {},
253
+ ...scoring.checkpoints ? { checkpoints: scoring.checkpoints } : {},
254
+ manifest: {
255
+ ...manifest,
256
+ schemaVersion: 7
257
+ },
258
+ ...summary ? { summary: {
259
+ ...summary,
260
+ ...isRecord(summary.scoring) ? { scoring: upgrade(summary.scoring) } : {}
261
+ } } : {}
262
+ }));
263
+ upgraded.set("run.json", encode({
264
+ ...run,
265
+ snapshotVersion: 3,
266
+ ...Array.isArray(scoring.metrics) && scoring.metrics.length ? { metrics: runMetrics(scoring.metrics) } : {}
267
+ }));
268
+ return upgraded;
269
+ }
270
+ /** True when the stored run is in snapshot format version 2. */
271
+ function isV2Run(runJson) {
272
+ return isRecord(runJson) && runJson.snapshotVersion === 2;
273
+ }
274
+ /** `run.json`'s metrics, as `evalkit share` derives them from the scoring's. */
275
+ function runMetrics(scoring) {
276
+ return scoring.filter(isRecord).map((metric) => ({
277
+ name: metric.name,
278
+ ...metric.unit ? { unit: metric.unit } : {},
279
+ ...metric.better ? { better: metric.better } : {},
280
+ ...metric.builtIn ? { builtIn: true } : {},
281
+ ...metric.value === void 0 ? {} : { value: metric.value }
282
+ }));
283
+ }
284
+ const decoder = new TextDecoder();
285
+ const encoder = new TextEncoder();
286
+ const parse = (data) => data ? JSON.parse(decoder.decode(data)) : void 0;
287
+ const encode = (value) => encoder.encode(`${JSON.stringify(value)}\n`);
288
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
289
+ function record$1(value, name) {
290
+ if (!isRecord(value)) throw new Error(`${name} must be an object`);
291
+ return value;
197
292
  }
198
293
  //#endregion
199
294
  //#region ../share/src/paths.ts
200
295
  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;
201
- const RUN_JSON = /^(detail|events|artifacts|workspace)\.json$/;
296
+ const RUN_JSON = /^(detail|transcript|artifacts|workspace)\.json$/;
297
+ const V2_RUN_JSON = /^(detail|events|artifacts|workspace)\.json$/;
202
298
  const RUN_FILE$1 = /^(?:workspace|attachments)\/(.+)$/;
203
299
  const LEGACY_TRIAL_JSON = /^trials\/([^/]+)\/(detail|events|artifacts|workspace)\.json$/;
204
300
  const LEGACY_TRIAL_FILE = /^trials\/([^/]+)\/(?:workspace|attachments)\/(.+)$/;
@@ -209,12 +305,13 @@ function safeSegment(segment) {
209
305
  return segment.length > 0 && segment.length <= 255 && segment !== "." && segment !== ".." && !/[\\/\0]/.test(segment);
210
306
  }
211
307
  /**
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.
308
+ * The only files a run bundle may contain (snapshot format version 3, from
309
+ * `evalkit share`, for one run): its JSON, candidate workspace files and
310
+ * attachments. Never evaluator workspaces. Version 2 (EvalKit 0.9 and
311
+ * 0.10) has `events.json` where version 3 has `transcript.json`.
215
312
  */
216
- function allowedRunPath(path) {
217
- if (path === "run.json" || path === "catalog.json" || RUN_JSON.test(path)) return true;
313
+ function allowedRunPath(path, version = 3) {
314
+ if (path === "run.json" || path === "catalog.json" || (version === 3 ? RUN_JSON : V2_RUN_JSON).test(path)) return true;
218
315
  const file = RUN_FILE$1.exec(path);
219
316
  return Boolean(file && file[1].split("/").every(safeSegment));
220
317
  }
@@ -255,18 +352,30 @@ function parseUpload(bundle) {
255
352
  }
256
353
  const runJson = byPath.get("run.json");
257
354
  if (!runJson) throw new Rejected("Bundle has no run.json");
258
- const legacy = isLegacyRun(parseJson(runJson, "run.json"));
259
- const allowed = legacy ? allowedLegacyRunPath : allowedRunPath;
355
+ const runValue = parseJson(runJson, "run.json");
356
+ const legacy = isLegacyRun(runValue);
357
+ const v2 = isV2Run(runValue);
358
+ const allowed = legacy ? allowedLegacyRunPath : (path) => allowedRunPath(path, v2 ? 2 : 3);
260
359
  for (const path of byPath.keys()) if (!allowed(path)) throw new Rejected(`File not allowed in a run bundle: ${path}`);
261
360
  const storedCatalog = byPath.get("catalog.json");
262
361
  if (storedCatalog) parseCatalog(storedCatalog);
263
362
  const catalog = storedCatalog && legacy ? upgradeCatalogFile(storedCatalog) : storedCatalog;
363
+ if (v2) return [runUpload(toV3(byPath), catalog)];
264
364
  if (!legacy) return [runUpload(byPath, catalog)];
265
365
  parseRun(runJson, { legacy: true });
266
366
  return upgradeSnapshotRun(byPath).filter(({ files }) => {
267
367
  const status = JSON.parse(new TextDecoder().decode(files.get("run.json"))).status;
268
368
  return status === "passed" || status === "failed" || status === "errored";
269
- }).map(({ files }) => runUpload(files, catalog));
369
+ }).map(({ files }) => runUpload(toV3(files), catalog));
370
+ }
371
+ /** A version 2 run as version 3; an upload that cannot be converted is refused. */
372
+ function toV3(files) {
373
+ parseRun(files.get("run.json"), { version: 2 });
374
+ try {
375
+ return upgradeV2Run(files);
376
+ } catch (error) {
377
+ throw new Rejected(`This run, from EvalKit 0.10 or older, could not be converted: ${error instanceof Error ? error.message : String(error)}. Upgrade the CLI and run \`evalkit upgrade\`, then share again.`);
378
+ }
270
379
  }
271
380
  function runUpload(files, catalog) {
272
381
  const runJson = files.get("run.json");
@@ -282,7 +391,8 @@ function runUpload(files, catalog) {
282
391
  }
283
392
  function parseRun(data, options = {}) {
284
393
  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");
394
+ const version = options.version ?? 3;
395
+ if (!options.legacy && value.snapshotVersion !== version) throw new Rejected(`run.json must be snapshot format version ${version}`);
286
396
  const source = record(value.source, "run.json source");
287
397
  const id = text(value.id, "id");
288
398
  if (!isUuid(id)) throw new Rejected("run.json id must be a UUID");
@@ -290,7 +400,7 @@ function parseRun(data, options = {}) {
290
400
  if (!isSha(commit)) throw new Rejected("source.commit must be a full 40-character SHA");
291
401
  if (typeof source.dirty !== "boolean") throw new Rejected("source.dirty must be a boolean");
292
402
  const status = value.status;
293
- if (status !== "passed" && status !== "failed" && status !== "errored") throw new Rejected(`Only finished runs can be shared (status ${JSON.stringify(status)})`);
403
+ if (status !== "passed" && status !== "failed" && status !== "errored" && status !== "limited") throw new Rejected(`Only finished runs can be shared (status ${JSON.stringify(status)})`);
294
404
  if (value.score !== void 0 && typeof value.score !== "number") throw new Rejected("score must be a number");
295
405
  const shared = {
296
406
  repo: text(source.repo, "source.repo"),
@@ -374,7 +484,7 @@ const jsonLine = (event) => console.log(JSON.stringify(event));
374
484
  *
375
485
  * POST /api/v1/uploads one run bundle
376
486
  * 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)
487
+ * POST /api/v1/upgrade convert stored runs from older EvalKit versions (RFD0008, RFD0009)
378
488
  * GET /api/v1/repos indexed repositories
379
489
  * GET /api/v1/repos/<repo>/-/commits commits, newest first
380
490
  * GET /api/v1/repos/<repo>/-/<sha>/data/… snapshot data
@@ -450,15 +560,20 @@ function createShareApp(options) {
450
560
  }, created ? 201 : 200);
451
561
  });
452
562
  /**
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.
563
+ * Converts stored runs from older EvalKit versions, `limit` runs per
564
+ * request (default 20), so it fits a Worker's limits. Call it until
565
+ * `remaining` is 0. Runs from before 0.9 (snapshot format version 1, a
566
+ * run of trials) become one run per trial (RFD0008); runs from 0.9 and
567
+ * 0.10 (version 2) get an ATXF transcript in place of their trajectory
568
+ * (RFD0009). New objects and index rows are written before old ones are
569
+ * removed, and run.json last, so an interrupted upgrade is finished by
570
+ * calling it again. A run that cannot be converted is left as it was and
571
+ * listed in `failed`.
458
572
  */
459
573
  app.post("/api/v1/upgrade", async (context) => {
460
574
  const limit = Math.min(Math.max(Number(context.req.query("limit") ?? 20) || 20, 1), 200);
461
575
  const legacy = [];
576
+ const v2 = [];
462
577
  for await (const key of files.list("repos/")) {
463
578
  if (!RUN_FILE.test(key)) continue;
464
579
  const data = await readBytes(await files.get(key));
@@ -470,21 +585,66 @@ function createShareApp(options) {
470
585
  continue;
471
586
  }
472
587
  if (isLegacyRun(value)) legacy.push(key);
588
+ else if (isV2Run(value)) v2.push(key);
473
589
  }
474
590
  let runs = 0;
591
+ let transcripts = 0;
592
+ const failed = [];
475
593
  const batch = legacy.slice(0, limit);
476
594
  for (const key of batch) runs += await upgradeStoredRun(key);
595
+ for (const key of v2.slice(0, limit - batch.length)) try {
596
+ await upgradeV2StoredRun(key);
597
+ transcripts += 1;
598
+ } catch (error) {
599
+ failed.push({
600
+ run: key,
601
+ reason: error instanceof Error ? error.message : String(error)
602
+ });
603
+ }
604
+ const done = batch.length + transcripts + failed.length;
477
605
  const result = {
478
606
  upgraded: batch.length,
479
607
  runs,
480
- remaining: legacy.length - batch.length
608
+ transcripts,
609
+ failed: failed.length,
610
+ remaining: legacy.length + v2.length - done
481
611
  };
482
612
  log({
483
613
  event: "share.upgrade",
484
614
  ...result
485
615
  });
486
- return context.json(result);
616
+ return context.json({
617
+ ...result,
618
+ failures: failed
619
+ });
487
620
  });
621
+ /** A stored version 2 run, converted to version 3 in place. */
622
+ async function upgradeV2StoredRun(runJsonKey) {
623
+ const runBase = runJsonKey.slice(0, -9);
624
+ const upgraded = upgradeV2Run(await v2Files(runBase));
625
+ for (const name of ["transcript.json", "detail.json"]) await files.put(`${runBase}/${name}`, upgraded.get(name));
626
+ const runJson = upgraded.get("run.json");
627
+ const run = storedRun(runJson);
628
+ if (run) {
629
+ await index.forgetRun(run.id);
630
+ await index.recordRun(run);
631
+ }
632
+ await files.delete(`${runBase}/events.json`);
633
+ await files.put(runJsonKey, runJson);
634
+ }
635
+ /** The files `upgradeV2Run` reads, from a stored run. */
636
+ async function v2Files(runBase) {
637
+ const stored = /* @__PURE__ */ new Map();
638
+ for (const name of [
639
+ "run.json",
640
+ "detail.json",
641
+ "events.json"
642
+ ]) {
643
+ const data = await readBytes(await files.get(`${runBase}/${name}`));
644
+ if (data) stored.set(name, data);
645
+ }
646
+ return stored;
647
+ }
488
648
  /** One stored version 1 run → version 2 runs; returns how many. */
489
649
  async function upgradeStoredRun(runJsonKey) {
490
650
  const [, repo, sha, oldRunId] = RUN_FILE.exec(runJsonKey);
@@ -501,6 +661,9 @@ function createShareApp(options) {
501
661
  const written = /* @__PURE__ */ new Set();
502
662
  let newRunJson;
503
663
  for (const upgraded of upgradeSnapshotRun(stored)) {
664
+ try {
665
+ upgraded.files = upgradeV2Run(upgraded.files);
666
+ } catch {}
504
667
  const runJson = upgraded.files.get("run.json");
505
668
  const run = storedRun(runJson);
506
669
  if (!run) continue;
@@ -666,9 +829,9 @@ function createShareApp(options) {
666
829
  const catalog = data ? parseCatalog(data) : EMPTY_CATALOG;
667
830
  return context.json(path === "catalog.json" ? { evals: catalog.catalog } : path === "evals.json" ? { evals: catalog.evals } : { suites: catalog.suites }, 200, NO_CACHE);
668
831
  }
669
- const runFile = /^runs\/([^/]+)\/((?:detail|events|artifacts|workspace)\.json|(?:workspace|attachments)\/.+)$/.exec(path);
832
+ const runFile = /^runs\/([^/]+)\/((?:detail|transcript|artifacts|workspace)\.json|(?:workspace|attachments)\/.+)$/.exec(path);
670
833
  if (!runFile || !isUuid(runFile[1])) return context.notFound();
671
- const body = await files.get(`${commitBase}/${path}`);
834
+ const body = runFile[2] === "transcript.json" || runFile[2] === "detail.json" ? await currentFile(`${commitBase}/runs/${runFile[1]}`, runFile[2]) : await files.get(`${commitBase}/${path}`);
672
835
  if (!body) return context.notFound();
673
836
  const json = path.endsWith(".json") && !path.includes("/workspace/") && !path.includes("/attachments/");
674
837
  return new Response(body, { headers: json ? {
@@ -681,14 +844,35 @@ function createShareApp(options) {
681
844
  "cache-control": IMMUTABLE
682
845
  } });
683
846
  }
847
+ /**
848
+ * A run's detail or transcript as version 3 has it: a version 2 run not
849
+ * upgraded yet is converted as it is read, the same way the upgrade does,
850
+ * so its cached files do not change when it is upgraded.
851
+ */
852
+ async function currentFile(runBase, name) {
853
+ const runJson = await readBytes(await files.get(`${runBase}/run.json`));
854
+ let value;
855
+ try {
856
+ value = runJson ? JSON.parse(new TextDecoder().decode(runJson)) : void 0;
857
+ } catch {
858
+ value = void 0;
859
+ }
860
+ if (!isV2Run(value)) return files.get(`${runBase}/${name}`);
861
+ try {
862
+ return upgradeV2Run(await v2Files(runBase)).get(name) ?? null;
863
+ } catch {
864
+ return name === "detail.json" ? files.get(`${runBase}/${name}`) : null;
865
+ }
866
+ }
684
867
  return app;
685
868
  }
686
- /** A stored run.json, or undefined if it is not a shareable run. */
869
+ /** A stored run.json (version 3, or 2 until upgraded), or undefined if it is not a shareable run. */
687
870
  function storedRun(data) {
688
871
  if (!data) return void 0;
689
872
  try {
690
- return parseRun(data);
873
+ return parseRun(data, { version: JSON.parse(new TextDecoder().decode(data)).snapshotVersion === 2 ? 2 : 3 });
691
874
  } catch (error) {
875
+ if (error instanceof SyntaxError) return void 0;
692
876
  if (error instanceof Rejected) return void 0;
693
877
  throw error;
694
878
  }
@@ -738,6 +922,12 @@ const MIGRATIONS = [
738
922
  `CREATE INDEX IF NOT EXISTS runs_by_eval ON runs (repo, eval_id, started_at)`
739
923
  ];
740
924
  /** Newest first; commits without a commit time order by first upload. */
925
+ /** Statuses a commit counts as failed: a limited run is a failure too. */
926
+ const FAILED = /* @__PURE__ */ new Set([
927
+ "failed",
928
+ "errored",
929
+ "limited"
930
+ ]);
741
931
  const BY_TIME = `ORDER BY COALESCE(committed_at, first_seen_at) DESC, first_seen_at DESC`;
742
932
  var ShareIndexDb = class {
743
933
  sql;
@@ -768,7 +958,7 @@ var ShareIndexDb = class {
768
958
  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);
769
959
  if (!run.source.dirty) this.sql.run(`UPDATE commits SET runs = runs + 1,
770
960
  passed = passed + ?, failed = failed + ?
771
- WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0, run.status === "failed" || run.status === "errored" ? 1 : 0, run.repoKey, run.source.commit);
961
+ WHERE repo = ? AND sha = ?`, run.status === "passed" ? 1 : 0, FAILED.has(run.status) ? 1 : 0, run.repoKey, run.source.commit);
772
962
  });
773
963
  }
774
964
  forgetRun(runId) {
@@ -778,7 +968,7 @@ var ShareIndexDb = class {
778
968
  this.sql.run("DELETE FROM runs WHERE id = ?", runId);
779
969
  if (!row.dirty) this.sql.run(`UPDATE commits SET runs = runs - 1,
780
970
  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);
971
+ WHERE repo = ? AND sha = ?`, row.status === "passed" ? 1 : 0, FAILED.has(row.status) ? 1 : 0, row.repo, row.sha);
782
972
  });
783
973
  }
784
974
  repos() {