@leostera/evalkit 0.3.0 → 0.5.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 +93 -0
- package/README.md +8 -7
- package/dist/cli.mjs +525 -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-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-M5eRlMfd.d.mts → index-lOw0fmak.d.mts} +96 -9
- package/dist/index-lOw0fmak.d.mts.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
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
import { a as readTrialEvents, c as readTrialSummary, i as readRunSummary, l as localReportStore, n as runMatrix, o as readTrialManifest, r as readRunManifest, t as runEval } from "./src-
|
|
2
|
+
import { I as defineEvalMatrix, L as canonicalParameters, m as registerEvals, n as authoringId } from "./src-ButXPY8P.mjs";
|
|
3
|
+
import { a as readTrialEvents, c as readTrialSummary, i as readRunSummary, l as localReportStore, n as runMatrix, o as readTrialManifest, r as readRunManifest, t as runEval } from "./src-DoPs9Yug.mjs";
|
|
4
|
+
import { c as dashboardIndex, d as encodeBundle, o as repoKey } from "./index-db-vbqEGFaU.mjs";
|
|
5
|
+
import { t as createDirectoryShareApp } from "./src-DzZueFn3.mjs";
|
|
4
6
|
import * as Schema from "effect/Schema";
|
|
5
7
|
import { Effect, LogLevel, Logger, Option } from "effect";
|
|
6
|
-
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cp, mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
7
9
|
import * as Path from "node:path";
|
|
8
|
-
import { basename, dirname, extname, relative, resolve, sep } from "node:path";
|
|
10
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
9
11
|
import * as OS from "node:os";
|
|
10
12
|
import * as Crypto from "node:crypto";
|
|
11
13
|
import * as NFS from "node:fs";
|
|
@@ -65,8 +67,9 @@ import * as FiberSet from "effect/FiberSet";
|
|
|
65
67
|
import "effect/Fiber";
|
|
66
68
|
import "effect/Pool";
|
|
67
69
|
import * as MutableRef from "effect/MutableRef";
|
|
68
|
-
import { parseArgs } from "node:util";
|
|
70
|
+
import { parseArgs, promisify } from "node:util";
|
|
69
71
|
import * as ChildProcess from "node:child_process";
|
|
72
|
+
import { execFile } from "node:child_process";
|
|
70
73
|
import * as RcRef from "effect/RcRef";
|
|
71
74
|
import * as readline from "node:readline";
|
|
72
75
|
import { Hono } from "hono";
|
|
@@ -3463,7 +3466,7 @@ function handleProcessInteger(options) {
|
|
|
3463
3466
|
};
|
|
3464
3467
|
}
|
|
3465
3468
|
/** @internal */
|
|
3466
|
-
const integer = (options) => {
|
|
3469
|
+
const integer$3 = (options) => {
|
|
3467
3470
|
const opts = {
|
|
3468
3471
|
min: Number.NEGATIVE_INFINITY,
|
|
3469
3472
|
max: Number.POSITIVE_INFINITY,
|
|
@@ -4029,6 +4032,12 @@ const choice = (alternatives) => {
|
|
|
4029
4032
|
return op;
|
|
4030
4033
|
};
|
|
4031
4034
|
/** @internal */
|
|
4035
|
+
const integer$2 = /*#__PURE__*/ (() => {
|
|
4036
|
+
const op = /*#__PURE__*/ Object.create(proto$5);
|
|
4037
|
+
op._tag = "Integer";
|
|
4038
|
+
return op;
|
|
4039
|
+
})();
|
|
4040
|
+
/** @internal */
|
|
4032
4041
|
const text$4 = /*#__PURE__*/ (() => {
|
|
4033
4042
|
const op = /*#__PURE__*/ Object.create(proto$5);
|
|
4034
4043
|
op._tag = "Text";
|
|
@@ -4184,7 +4193,7 @@ const wizardInternal$3 = (self, help) => {
|
|
|
4184
4193
|
case "Integer": {
|
|
4185
4194
|
const primitiveHelp = p("Enter an integer");
|
|
4186
4195
|
const message = sequence(help, primitiveHelp);
|
|
4187
|
-
return integer({ message: toAnsiText(message).trimEnd() }).pipe(map$3((value) => `${value}`));
|
|
4196
|
+
return integer$3({ message: toAnsiText(message).trimEnd() }).pipe(map$3((value) => `${value}`));
|
|
4188
4197
|
}
|
|
4189
4198
|
case "Path": {
|
|
4190
4199
|
const primitiveHelp = p("Select a file system path");
|
|
@@ -4689,7 +4698,7 @@ const wizardInternal$2 = (self, config) => {
|
|
|
4689
4698
|
case "Variadic": {
|
|
4690
4699
|
const repeatHelp = p("How many times should this argument should be repeated?");
|
|
4691
4700
|
const message = pipe(getHelpInternal$2(self), sequence(repeatHelp));
|
|
4692
|
-
return integer({
|
|
4701
|
+
return integer$3({
|
|
4693
4702
|
message: toAnsiText(message).trimEnd(),
|
|
4694
4703
|
min: getMinSizeInternal$1(self),
|
|
4695
4704
|
max: getMaxSizeInternal$1(self)
|
|
@@ -4874,13 +4883,15 @@ const boolean$1 = (name, options) => {
|
|
|
4874
4883
|
const head = Arr.headNonEmpty(negationNames);
|
|
4875
4884
|
const tail = Arr.tailNonEmpty(negationNames);
|
|
4876
4885
|
const negationOption = makeSingle(head, tail, boolean$2(Option$1.some(!ifPresent)));
|
|
4877
|
-
return withDefault(orElse(option, negationOption), !ifPresent);
|
|
4886
|
+
return withDefault$1(orElse(option, negationOption), !ifPresent);
|
|
4878
4887
|
}
|
|
4879
|
-
return withDefault(option, !ifPresent);
|
|
4888
|
+
return withDefault$1(option, !ifPresent);
|
|
4880
4889
|
};
|
|
4881
4890
|
/** @internal */
|
|
4882
4891
|
const choiceWithValue = (name, choices) => makeSingle(name, Arr.empty(), choice(choices));
|
|
4883
4892
|
/** @internal */
|
|
4893
|
+
const integer$1 = (name) => makeSingle(name, Arr.empty(), integer$2);
|
|
4894
|
+
/** @internal */
|
|
4884
4895
|
const none = /*#__PURE__*/ (() => {
|
|
4885
4896
|
const op = /*#__PURE__*/ Object.create(proto$2);
|
|
4886
4897
|
op._tag = "Empty";
|
|
@@ -4895,7 +4906,7 @@ const getUsage$1 = (self) => getUsageInternal$1(self);
|
|
|
4895
4906
|
/** @internal */
|
|
4896
4907
|
const map$1 = /*#__PURE__*/ dual(2, (self, f) => makeMap(self, (a) => Either$1.right(f(a))));
|
|
4897
4908
|
/** @internal */
|
|
4898
|
-
const optional$1 = (self) => withDefault(map$1(self, Option$1.some), Option$1.none());
|
|
4909
|
+
const optional$1 = (self) => withDefault$1(map$1(self, Option$1.some), Option$1.none());
|
|
4899
4910
|
/** @internal */
|
|
4900
4911
|
const orElse = /*#__PURE__*/ dual(2, (self, that) => orElseEither(self, that).pipe(map$1(Either$1.merge)));
|
|
4901
4912
|
/** @internal */
|
|
@@ -4917,7 +4928,7 @@ const withAlias = /*#__PURE__*/ dual(2, (self, alias) => modifySingle(self, (sin
|
|
|
4917
4928
|
return makeSingle(single.name, aliases, single.primitiveType, single.description, single.pseudoName);
|
|
4918
4929
|
}));
|
|
4919
4930
|
/** @internal */
|
|
4920
|
-
const withDefault = /*#__PURE__*/ dual(2, (self, fallback) => makeWithDefault(self, fallback));
|
|
4931
|
+
const withDefault$1 = /*#__PURE__*/ dual(2, (self, fallback) => makeWithDefault(self, fallback));
|
|
4921
4932
|
/** @internal */
|
|
4922
4933
|
const withDescription = /*#__PURE__*/ dual(2, (self, desc) => modifySingle(self, (single) => {
|
|
4923
4934
|
const description = sequence(single.description, p(desc));
|
|
@@ -5305,7 +5316,7 @@ const wizardInternal$1 = (self, config) => {
|
|
|
5305
5316
|
case "Variadic": {
|
|
5306
5317
|
const repeatHelp = p("How many times should this argument be repeated?");
|
|
5307
5318
|
const message = pipe(getHelpInternal$1(self), sequence(repeatHelp));
|
|
5308
|
-
return integer({
|
|
5319
|
+
return integer$3({
|
|
5309
5320
|
message: toAnsiText(message).trimEnd(),
|
|
5310
5321
|
min: getMinSizeInternal(self),
|
|
5311
5322
|
max: getMaxSizeInternal(self)
|
|
@@ -5957,6 +5968,11 @@ const boolean = boolean$1;
|
|
|
5957
5968
|
* @since 1.0.0
|
|
5958
5969
|
* @category constructors
|
|
5959
5970
|
*/
|
|
5971
|
+
const integer = integer$1;
|
|
5972
|
+
/**
|
|
5973
|
+
* @since 1.0.0
|
|
5974
|
+
* @category constructors
|
|
5975
|
+
*/
|
|
5960
5976
|
const text = text$1;
|
|
5961
5977
|
/**
|
|
5962
5978
|
* @since 1.0.0
|
|
@@ -5974,6 +5990,11 @@ const optional = optional$1;
|
|
|
5974
5990
|
* @category combinators
|
|
5975
5991
|
*/
|
|
5976
5992
|
const repeated = repeated$1;
|
|
5993
|
+
/**
|
|
5994
|
+
* @since 1.0.0
|
|
5995
|
+
* @category combinators
|
|
5996
|
+
*/
|
|
5997
|
+
const withDefault = withDefault$1;
|
|
5977
5998
|
//#endregion
|
|
5978
5999
|
//#region ../../node_modules/.bun/@effect+cli@0.77.2+74e017304edf4bf0/node_modules/@effect/cli/dist/esm/internal/commandDirective.js
|
|
5979
6000
|
/** @internal */
|
|
@@ -8126,6 +8147,7 @@ function createDashboardReportReader(root, registry) {
|
|
|
8126
8147
|
return {
|
|
8127
8148
|
id,
|
|
8128
8149
|
...await ifPresent(() => stat(resolve(root, id, "archived.json"))) ? { archived: true } : {},
|
|
8150
|
+
...manifest.source ? { source: manifest.source } : {},
|
|
8129
8151
|
evalId: manifest.evalId,
|
|
8130
8152
|
...manifest.suiteId ? { suiteId: manifest.suiteId } : {},
|
|
8131
8153
|
...manifest.matrix ? {
|
|
@@ -8244,7 +8266,7 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8244
8266
|
await prepareOutDir(outDir);
|
|
8245
8267
|
await cp(dashboardRoot, outDir, { recursive: true });
|
|
8246
8268
|
await writeFile(resolve(outDir, MARKER), "Generated by `evalkit build`.\n");
|
|
8247
|
-
await writeFile(resolve(outDir, "index.html"),
|
|
8269
|
+
await writeFile(resolve(outDir, "index.html"), dashboardIndex(template, { base: "./" }));
|
|
8248
8270
|
}, () => "read-only UI with relative assets");
|
|
8249
8271
|
await step("Eval catalog", async () => {
|
|
8250
8272
|
await write("catalog.json", { evals: registry.catalog() });
|
|
@@ -8265,13 +8287,10 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8265
8287
|
const trialIds = [];
|
|
8266
8288
|
for (const [index, run] of runs.entries()) {
|
|
8267
8289
|
progress(`run ${index + 1}/${runs.length} · ${run.id.slice(0, 8)}`);
|
|
8268
|
-
const
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
trialIds.push(trial.id);
|
|
8273
|
-
files += await snapshotTrial(reports, write, data, run.id, trial.id);
|
|
8274
|
-
}
|
|
8290
|
+
const written = await snapshotRun(reports, run.id, directorySink(resolve(data, "runs", run.id)));
|
|
8291
|
+
trials += written.trialIds.length;
|
|
8292
|
+
files += written.files;
|
|
8293
|
+
trialIds.push(...written.trialIds);
|
|
8275
8294
|
}
|
|
8276
8295
|
return {
|
|
8277
8296
|
runs: runs.length,
|
|
@@ -8293,9 +8312,9 @@ async function buildDashboard(options, step = quietStep) {
|
|
|
8293
8312
|
for (const route of routes) {
|
|
8294
8313
|
const file = resolve(outDir, `${route}.html`);
|
|
8295
8314
|
await mkdir(dirname(file), { recursive: true });
|
|
8296
|
-
await writeFile(file,
|
|
8315
|
+
await writeFile(file, dashboardIndex(template, { base: baseFor(route) }));
|
|
8297
8316
|
}
|
|
8298
|
-
await writeFile(resolve(outDir, "404.html"),
|
|
8317
|
+
await writeFile(resolve(outDir, "404.html"), dashboardIndex(template, { base: "/" }));
|
|
8299
8318
|
return routes.length;
|
|
8300
8319
|
}, (pages) => `${count(pages, "page")} with clean /path URLs`);
|
|
8301
8320
|
const { runIds: _runs, trialIds: _trials, ...summary } = snapshot;
|
|
@@ -8333,26 +8352,55 @@ function baseFor(route) {
|
|
|
8333
8352
|
function count(value, noun) {
|
|
8334
8353
|
return `${value} ${noun}${value === 1 ? "" : "s"}`;
|
|
8335
8354
|
}
|
|
8336
|
-
/** Writes
|
|
8337
|
-
|
|
8338
|
-
const
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8355
|
+
/** Writes a run's snapshot below `root` (static builds). */
|
|
8356
|
+
function directorySink(root) {
|
|
8357
|
+
const target = (path) => {
|
|
8358
|
+
const file = resolve(root, path);
|
|
8359
|
+
if (!file.startsWith(`${resolve(root)}${sep}`)) throw new Error(`Invalid snapshot path: ${path}`);
|
|
8360
|
+
return file;
|
|
8361
|
+
};
|
|
8362
|
+
return {
|
|
8363
|
+
async json(path, value) {
|
|
8364
|
+
const file = target(path);
|
|
8365
|
+
await mkdir(dirname(file), { recursive: true });
|
|
8366
|
+
await writeFile(file, `${JSON.stringify(value)}\n`);
|
|
8367
|
+
},
|
|
8368
|
+
async bytes(path, data) {
|
|
8369
|
+
const file = target(path);
|
|
8370
|
+
await mkdir(dirname(file), { recursive: true });
|
|
8371
|
+
await writeFile(file, data);
|
|
8372
|
+
}
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
/**
|
|
8376
|
+
* One run's snapshot, shared by `evalkit build` (files on disk) and
|
|
8377
|
+
* `evalkit share` (an upload bundle): `trials.json`, then per trial
|
|
8378
|
+
* `detail.json`, `events.json`, `artifacts.json`, `workspace.json`, and the
|
|
8379
|
+
* candidate files under `workspace/`. Evaluator workspaces are never read.
|
|
8380
|
+
*/
|
|
8381
|
+
async function snapshotRun(reports, runId, sink, options = {}) {
|
|
8382
|
+
const trials = await reports.listTrials(runId);
|
|
8383
|
+
await sink.json("trials.json", { trials });
|
|
8345
8384
|
let files = 0;
|
|
8346
|
-
for (const
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
await
|
|
8352
|
-
await
|
|
8353
|
-
|
|
8385
|
+
for (const trial of trials) {
|
|
8386
|
+
const base = `trials/${trial.id}`;
|
|
8387
|
+
await sink.json(`${base}/detail.json`, await reports.trialDetail(runId, trial.id));
|
|
8388
|
+
await sink.json(`${base}/events.json`, { events: await reports.trajectory(runId, trial.id) });
|
|
8389
|
+
await sink.json(`${base}/artifacts.json`, { artifacts: await reports.listArtifacts(runId, trial.id) });
|
|
8390
|
+
const workspace = await reports.listCandidateWorkspace(runId, trial.id);
|
|
8391
|
+
await sink.json(`${base}/workspace.json`, workspace);
|
|
8392
|
+
if (options.workspaceFiles === false) continue;
|
|
8393
|
+
for (const entry of workspace) {
|
|
8394
|
+
if (entry.kind !== "file") continue;
|
|
8395
|
+
const response = await reports.candidateFile(runId, trial.id, entry.path);
|
|
8396
|
+
await sink.bytes(`${base}/workspace/${entry.path}`, new Uint8Array(await response.arrayBuffer()));
|
|
8397
|
+
files += 1;
|
|
8398
|
+
}
|
|
8354
8399
|
}
|
|
8355
|
-
return
|
|
8400
|
+
return {
|
|
8401
|
+
trialIds: trials.map((trial) => trial.id),
|
|
8402
|
+
files
|
|
8403
|
+
};
|
|
8356
8404
|
}
|
|
8357
8405
|
/** Total size and file count of a directory, for the build summary. */
|
|
8358
8406
|
async function directorySize(root) {
|
|
@@ -8371,15 +8419,6 @@ async function directorySize(root) {
|
|
|
8371
8419
|
files
|
|
8372
8420
|
};
|
|
8373
8421
|
}
|
|
8374
|
-
/**
|
|
8375
|
-
* Select the static API and make every URL relative to `<base href>`, which
|
|
8376
|
-
* points at the snapshot root. The app derives its router basename from it.
|
|
8377
|
-
*/
|
|
8378
|
-
function staticIndex(html, base = "./") {
|
|
8379
|
-
const result = html.replace("<html lang=\"en\">", "<html lang=\"en\" data-evalkit-snapshot=\"static\">").replace("<head>", `<head>\n <base href="${base}" />`).replaceAll("\"/assets/", "\"./assets/");
|
|
8380
|
-
if (!result.includes("data-evalkit-snapshot=\"static\"") || !result.includes("<base href=")) throw new Error("Bundled dashboard index.html has an unexpected shape");
|
|
8381
|
-
return result;
|
|
8382
|
-
}
|
|
8383
8422
|
async function prepareOutDir(outDir) {
|
|
8384
8423
|
let entries;
|
|
8385
8424
|
try {
|
|
@@ -8678,6 +8717,126 @@ async function confirm(question) {
|
|
|
8678
8717
|
}
|
|
8679
8718
|
}
|
|
8680
8719
|
//#endregion
|
|
8720
|
+
//#region ../cli/src/source.ts
|
|
8721
|
+
const exec = promisify(execFile);
|
|
8722
|
+
/**
|
|
8723
|
+
* Normalize a git remote to a stable repository key: lowercase host, no
|
|
8724
|
+
* credentials, port, scheme, `.git`, or trailing slash.
|
|
8725
|
+
*
|
|
8726
|
+
* git@GitLab.example.com:group/sub/repo.git → gitlab.example.com/group/sub/repo
|
|
8727
|
+
* https://user:token@github.com/o/r/ → github.com/o/r
|
|
8728
|
+
* /srv/repos/thing.git (no host) → local/thing
|
|
8729
|
+
*/
|
|
8730
|
+
function normalizeRepoUrl(url) {
|
|
8731
|
+
const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
8732
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/.exec(trimmed);
|
|
8733
|
+
if (scp && !/^[a-z]+:\/\//i.test(trimmed)) return `${scp[1].toLowerCase()}/${scp[2].replace(/^\/+/, "")}`;
|
|
8734
|
+
try {
|
|
8735
|
+
const parsed = new URL(trimmed);
|
|
8736
|
+
if (parsed.protocol !== "file:" && parsed.hostname) return `${parsed.hostname.toLowerCase()}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
8737
|
+
} catch {}
|
|
8738
|
+
return `local/${basename(trimmed)}`;
|
|
8739
|
+
}
|
|
8740
|
+
async function git(cwd, ...args) {
|
|
8741
|
+
try {
|
|
8742
|
+
const { stdout } = await exec("git", [
|
|
8743
|
+
"-C",
|
|
8744
|
+
cwd,
|
|
8745
|
+
...args
|
|
8746
|
+
], { timeout: 1e4 });
|
|
8747
|
+
return stdout.trim() || void 0;
|
|
8748
|
+
} catch {
|
|
8749
|
+
return;
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
const posix = (path) => path.split(sep).join("/");
|
|
8753
|
+
const projectPath = (root, projectRoot) => root ? posix(relative(root, projectRoot)) : "";
|
|
8754
|
+
function ciSource(env, projectRoot) {
|
|
8755
|
+
if (env.EVALKIT_SOURCE_REPO && env.EVALKIT_SOURCE_COMMIT) return {
|
|
8756
|
+
repo: normalizeRepoUrl(env.EVALKIT_SOURCE_REPO),
|
|
8757
|
+
project: env.EVALKIT_SOURCE_PROJECT ?? "",
|
|
8758
|
+
commit: env.EVALKIT_SOURCE_COMMIT,
|
|
8759
|
+
...env.EVALKIT_SOURCE_REF ? { ref: env.EVALKIT_SOURCE_REF } : {},
|
|
8760
|
+
...env.EVALKIT_SOURCE_COMMITTED_AT ? { committedAt: env.EVALKIT_SOURCE_COMMITTED_AT } : {}
|
|
8761
|
+
};
|
|
8762
|
+
if (env.GITLAB_CI === "true" && env.CI_PROJECT_URL && env.CI_COMMIT_SHA) return {
|
|
8763
|
+
repo: normalizeRepoUrl(env.CI_PROJECT_URL),
|
|
8764
|
+
project: projectPath(env.CI_PROJECT_DIR, projectRoot),
|
|
8765
|
+
commit: env.CI_COMMIT_SHA,
|
|
8766
|
+
...env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME ? { ref: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? env.CI_COMMIT_REF_NAME } : {},
|
|
8767
|
+
...env.CI_COMMIT_TIMESTAMP ? { committedAt: env.CI_COMMIT_TIMESTAMP } : {},
|
|
8768
|
+
ci: {
|
|
8769
|
+
provider: "gitlab",
|
|
8770
|
+
...env.CI_PIPELINE_ID ? { pipeline: env.CI_PIPELINE_ID } : {},
|
|
8771
|
+
...env.CI_JOB_ID ? { job: env.CI_JOB_ID } : {},
|
|
8772
|
+
...env.CI_JOB_URL ? { url: env.CI_JOB_URL } : {}
|
|
8773
|
+
}
|
|
8774
|
+
};
|
|
8775
|
+
if (env.GITHUB_ACTIONS === "true" && env.GITHUB_REPOSITORY && env.GITHUB_SHA) {
|
|
8776
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
8777
|
+
return {
|
|
8778
|
+
repo: normalizeRepoUrl(`${server}/${env.GITHUB_REPOSITORY}`),
|
|
8779
|
+
project: projectPath(env.GITHUB_WORKSPACE, projectRoot),
|
|
8780
|
+
commit: env.GITHUB_SHA,
|
|
8781
|
+
...env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME ? { ref: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME } : {},
|
|
8782
|
+
ci: {
|
|
8783
|
+
provider: "github",
|
|
8784
|
+
...env.GITHUB_RUN_ID ? { pipeline: env.GITHUB_RUN_ID } : {},
|
|
8785
|
+
...env.GITHUB_JOB ? { job: env.GITHUB_JOB } : {},
|
|
8786
|
+
...env.GITHUB_RUN_ID ? { url: `${server}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` } : {}
|
|
8787
|
+
}
|
|
8788
|
+
};
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
/**
|
|
8792
|
+
* Determine which repository and commit this project's eval code comes from.
|
|
8793
|
+
* Order: EVALKIT_SOURCE_* variables, GitLab/GitHub CI variables, then git.
|
|
8794
|
+
* Returns undefined outside git with no CI variables ("untracked" results).
|
|
8795
|
+
*
|
|
8796
|
+
* `outputDirs` (results, sandbox, snapshot) never make the tree dirty, even
|
|
8797
|
+
* when a project forgot to ignore them.
|
|
8798
|
+
*/
|
|
8799
|
+
async function detectSource(givenRoot, options = {}) {
|
|
8800
|
+
const env = options.env ?? process.env;
|
|
8801
|
+
const projectRoot = await realpath(givenRoot).catch(() => givenRoot);
|
|
8802
|
+
const top = await git(projectRoot, "rev-parse", "--show-toplevel");
|
|
8803
|
+
const excludes = top ? (options.outputDirs ?? []).map((dir) => join(projectRoot, relative(givenRoot, dir))).map((dir) => posix(relative(top, dir))).filter((dir) => dir && !dir.startsWith("..")).map((dir) => `:(exclude)${dir}`) : [];
|
|
8804
|
+
const dirty = top ? Boolean(await git(top, "status", "--porcelain", "--", ".", ...excludes)) : false;
|
|
8805
|
+
const fromCi = ciSource(env, projectRoot);
|
|
8806
|
+
if (fromCi) return {
|
|
8807
|
+
...fromCi,
|
|
8808
|
+
...top && !env.EVALKIT_SOURCE_PROJECT ? { project: projectPath(top, projectRoot) } : {},
|
|
8809
|
+
dirty
|
|
8810
|
+
};
|
|
8811
|
+
if (!top) return void 0;
|
|
8812
|
+
const commit = await git(top, "rev-parse", "HEAD");
|
|
8813
|
+
if (!commit) return void 0;
|
|
8814
|
+
const remote = await git(top, "config", "--get", "remote.origin.url") ?? (await git(top, "remote"))?.split("\n").map((name) => name.trim()).find(Boolean);
|
|
8815
|
+
const remoteUrl = remote && !remote.includes("/") && !remote.includes(":") ? await git(top, "config", "--get", `remote.${remote}.url`) : remote;
|
|
8816
|
+
const ref = await git(top, "symbolic-ref", "--short", "-q", "HEAD");
|
|
8817
|
+
const committedAt = await git(top, "show", "-s", "--format=%cI", "HEAD");
|
|
8818
|
+
return {
|
|
8819
|
+
repo: normalizeRepoUrl(remoteUrl ?? top),
|
|
8820
|
+
project: projectPath(top, projectRoot),
|
|
8821
|
+
commit,
|
|
8822
|
+
...ref ? { ref } : {},
|
|
8823
|
+
...committedAt ? { committedAt } : {},
|
|
8824
|
+
dirty
|
|
8825
|
+
};
|
|
8826
|
+
}
|
|
8827
|
+
/** The project's evals and suites, saved with each run (`catalog.json`). */
|
|
8828
|
+
function runCatalog(registry) {
|
|
8829
|
+
return {
|
|
8830
|
+
catalog: registry.catalog(),
|
|
8831
|
+
evals: registry.metadata(),
|
|
8832
|
+
suites: registry.suiteMetadata()
|
|
8833
|
+
};
|
|
8834
|
+
}
|
|
8835
|
+
/** `abc1234` or `abc1234+dirty`. */
|
|
8836
|
+
function shortSource(source) {
|
|
8837
|
+
return `${source.commit.slice(0, 7)}${source.dirty ? "+dirty" : ""}`;
|
|
8838
|
+
}
|
|
8839
|
+
//#endregion
|
|
8681
8840
|
//#region ../cli/src/run-command.ts
|
|
8682
8841
|
function parseRunArgs(args) {
|
|
8683
8842
|
const { values, positionals } = parseArgs({
|
|
@@ -8824,15 +8983,24 @@ async function runProjectCommand(args, cwd = process.cwd(), parsed = parseRunArg
|
|
|
8824
8983
|
return;
|
|
8825
8984
|
}
|
|
8826
8985
|
if (cells > (project.config.execution?.maxCells ?? 100) && !values.all) throw new Error(`Selected ${cells} cells. Narrow the selection, inspect --dry-run, or pass --all.`);
|
|
8827
|
-
|
|
8986
|
+
const reportDir = resolve(project.root, project.config.reportDir ?? "_evalkit-results");
|
|
8987
|
+
const sandboxDir = resolve(project.root, project.config.sandboxDir ?? "_evalkit-sandbox");
|
|
8988
|
+
const source = await detectSource(project.root, { outputDirs: [
|
|
8989
|
+
reportDir,
|
|
8990
|
+
sandboxDir,
|
|
8991
|
+
resolve(project.root, "_evalkit-dashboard")
|
|
8992
|
+
] });
|
|
8993
|
+
if (!values.json) console.log(`Running ${cells} cell(s) from ${plan.matrix}` + (source ? ` at ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : " (untracked: not in a git repository)"));
|
|
8828
8994
|
if ((await Effect.runPromise(runnerLogs(runMatrix(matrix, {
|
|
8829
8995
|
selection,
|
|
8830
8996
|
concurrency: parsed.concurrency ?? project.config.execution?.concurrency ?? 4,
|
|
8831
8997
|
trials,
|
|
8832
8998
|
...suiteId ? { suiteId } : {},
|
|
8833
8999
|
...values.local ? { runtime: "local" } : {},
|
|
8834
|
-
|
|
8835
|
-
|
|
9000
|
+
...source ? { source } : {},
|
|
9001
|
+
catalog: runCatalog(registry),
|
|
9002
|
+
report: localReportStore(reportDir),
|
|
9003
|
+
workspaceRoot: sandboxDir,
|
|
8836
9004
|
onResult: (cell, result) => {
|
|
8837
9005
|
if (values.json) console.log(JSON.stringify({
|
|
8838
9006
|
cell: {
|
|
@@ -8903,6 +9071,205 @@ async function tidyReports(options) {
|
|
|
8903
9071
|
return result;
|
|
8904
9072
|
}
|
|
8905
9073
|
//#endregion
|
|
9074
|
+
//#region ../cli/src/share-command.ts
|
|
9075
|
+
/**
|
|
9076
|
+
* Which local runs `evalkit share` uploads: finished runs with a source, at
|
|
9077
|
+
* HEAD (or any commit with `all`), not dirty (unless `allowDirty`), not archived.
|
|
9078
|
+
*/
|
|
9079
|
+
function selectRuns(runs, head, options) {
|
|
9080
|
+
const result = {
|
|
9081
|
+
selected: [],
|
|
9082
|
+
untracked: 0,
|
|
9083
|
+
otherCommits: 0,
|
|
9084
|
+
dirty: 0,
|
|
9085
|
+
archived: 0,
|
|
9086
|
+
running: 0
|
|
9087
|
+
};
|
|
9088
|
+
for (const run of runs) {
|
|
9089
|
+
const { source } = run;
|
|
9090
|
+
if (!source) result.untracked += 1;
|
|
9091
|
+
else if (!options.all && (source.commit !== head.commit || repoKey(source) !== repoKey(head))) result.otherCommits += 1;
|
|
9092
|
+
else if (source.dirty && !options.allowDirty) result.dirty += 1;
|
|
9093
|
+
else if (run.archived) result.archived += 1;
|
|
9094
|
+
else if (run.status === "running") result.running += 1;
|
|
9095
|
+
else result.selected.push({
|
|
9096
|
+
...run,
|
|
9097
|
+
source
|
|
9098
|
+
});
|
|
9099
|
+
}
|
|
9100
|
+
return result;
|
|
9101
|
+
}
|
|
9102
|
+
/** One line for the Runs step: what will be shared and what was skipped. */
|
|
9103
|
+
function selectionSummary(selection, options) {
|
|
9104
|
+
return [
|
|
9105
|
+
`${plural(selection.selected.length, "run")} to share`,
|
|
9106
|
+
selection.otherCommits && !options.all ? `${selection.otherCommits} from other commits` : "",
|
|
9107
|
+
selection.running ? `${selection.running} still running skipped` : "",
|
|
9108
|
+
selection.dirty ? `${selection.dirty} dirty skipped` : "",
|
|
9109
|
+
selection.untracked ? `${selection.untracked} untracked skipped` : "",
|
|
9110
|
+
selection.archived ? `${selection.archived} archived skipped` : ""
|
|
9111
|
+
].filter(Boolean).join(" · ");
|
|
9112
|
+
}
|
|
9113
|
+
/** Why nothing was selected, and what to do about it. */
|
|
9114
|
+
function noRunsMessage(selection, head, options) {
|
|
9115
|
+
const at = shortSource(head);
|
|
9116
|
+
if (selection.dirty) return `No committed runs for ${at}: ${plural(selection.dirty, "run")} came from uncommitted changes. Commit and rerun \`evalkit run\`, or pass --allow-dirty.`;
|
|
9117
|
+
if (selection.running) return `No finished runs for ${at}: ${plural(selection.running, "run")} still running. Share again when they finish.`;
|
|
9118
|
+
return `No runs for ${at}. Run \`evalkit run\` first${options.all ? "" : ", or pass --all to share runs from other commits"}.`;
|
|
9119
|
+
}
|
|
9120
|
+
/** Where to look after sharing: the commit, or the repository if several commits were shared. */
|
|
9121
|
+
function shareTarget(serverUrl, head, runs) {
|
|
9122
|
+
const commits = new Set(runs.map((run) => run.source.commit));
|
|
9123
|
+
const commit = commits.size === 1 ? `/-/${[...commits][0]}` : "";
|
|
9124
|
+
return new URL(`/${repoKey(head)}${commit}`, serverUrl).href;
|
|
9125
|
+
}
|
|
9126
|
+
/** Build one run's upload bundle: run.json, catalog.json, and its snapshot. */
|
|
9127
|
+
async function runBundle(options) {
|
|
9128
|
+
const files = [];
|
|
9129
|
+
const encode = (value) => new TextEncoder().encode(`${JSON.stringify(value)}\n`);
|
|
9130
|
+
files.push({
|
|
9131
|
+
path: "run.json",
|
|
9132
|
+
data: encode(options.run)
|
|
9133
|
+
});
|
|
9134
|
+
const saved = await readFile(resolve(options.reportRoot, options.run.id, "catalog.json")).catch(() => void 0);
|
|
9135
|
+
files.push({
|
|
9136
|
+
path: "catalog.json",
|
|
9137
|
+
data: saved ? new Uint8Array(saved) : encode(runCatalog(options.registry))
|
|
9138
|
+
});
|
|
9139
|
+
await snapshotRun(options.reports, options.run.id, {
|
|
9140
|
+
async json(path, value) {
|
|
9141
|
+
files.push({
|
|
9142
|
+
path,
|
|
9143
|
+
data: encode(value)
|
|
9144
|
+
});
|
|
9145
|
+
},
|
|
9146
|
+
async bytes(path, data) {
|
|
9147
|
+
files.push({
|
|
9148
|
+
path,
|
|
9149
|
+
data
|
|
9150
|
+
});
|
|
9151
|
+
}
|
|
9152
|
+
}, { workspaceFiles: options.workspaceFiles });
|
|
9153
|
+
return encodeBundle(files);
|
|
9154
|
+
}
|
|
9155
|
+
/** The server refused this bundle (4xx): retrying it cannot help. */
|
|
9156
|
+
var UploadRejected = class extends Error {};
|
|
9157
|
+
/**
|
|
9158
|
+
* The URL does not answer as an EvalKit share server (a redirect, an HTML page,
|
|
9159
|
+
* an auth proxy). Every upload would fail the same way, so sharing stops.
|
|
9160
|
+
*/
|
|
9161
|
+
var ShareServerError = class extends Error {};
|
|
9162
|
+
/** `share.headers` from the config: called once, checked, never printed. */
|
|
9163
|
+
async function resolveShareHeaders(headers) {
|
|
9164
|
+
let value;
|
|
9165
|
+
try {
|
|
9166
|
+
value = typeof headers === "function" ? await headers() : headers ?? {};
|
|
9167
|
+
} catch (error) {
|
|
9168
|
+
throw new Error(`share.headers failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9169
|
+
}
|
|
9170
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("share.headers must be an object of header names to values");
|
|
9171
|
+
const entries = Object.entries(value);
|
|
9172
|
+
for (const [name, item] of entries) if (typeof item !== "string" || !item) throw new Error(`share.headers "${name}" must be a non-empty string`);
|
|
9173
|
+
return Object.fromEntries(entries);
|
|
9174
|
+
}
|
|
9175
|
+
/** Check that the URL is an EvalKit share server and the headers get through. */
|
|
9176
|
+
async function checkShareServer(serverUrl, headers) {
|
|
9177
|
+
const response = await fetch(new URL("/api/v1/repos", serverUrl), {
|
|
9178
|
+
headers,
|
|
9179
|
+
redirect: "manual"
|
|
9180
|
+
});
|
|
9181
|
+
await expectShareServer(response, serverUrl);
|
|
9182
|
+
await response.body?.cancel();
|
|
9183
|
+
}
|
|
9184
|
+
/** POST one bundle, retrying network errors and 5xx responses. */
|
|
9185
|
+
async function uploadBundle(serverUrl, bundle, options = {}) {
|
|
9186
|
+
const attempts = options.attempts ?? 3;
|
|
9187
|
+
let lastError;
|
|
9188
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
9189
|
+
try {
|
|
9190
|
+
return await postBundle(serverUrl, bundle, options.headers ?? {});
|
|
9191
|
+
} catch (error) {
|
|
9192
|
+
if (error instanceof UploadRejected || error instanceof ShareServerError) throw error;
|
|
9193
|
+
lastError = error;
|
|
9194
|
+
}
|
|
9195
|
+
if (attempt < attempts) await Bun.sleep((options.delayMs ?? 500) * 2 ** (attempt - 1));
|
|
9196
|
+
}
|
|
9197
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
|
|
9198
|
+
}
|
|
9199
|
+
async function postBundle(serverUrl, bundle, headers) {
|
|
9200
|
+
const response = await fetch(new URL("/api/v1/uploads", serverUrl), {
|
|
9201
|
+
method: "POST",
|
|
9202
|
+
headers: {
|
|
9203
|
+
...headers,
|
|
9204
|
+
"content-type": "application/octet-stream"
|
|
9205
|
+
},
|
|
9206
|
+
body: bundle,
|
|
9207
|
+
redirect: "manual"
|
|
9208
|
+
});
|
|
9209
|
+
await expectShareServer(response, serverUrl);
|
|
9210
|
+
if (response.ok) return parseUploadResult(await response.json());
|
|
9211
|
+
const reason = await response.text();
|
|
9212
|
+
if (response.status < 500) throw new UploadRejected(`Share server rejected the upload: ${reason}`);
|
|
9213
|
+
throw new Error(`Share server error ${response.status}: ${reason}`);
|
|
9214
|
+
}
|
|
9215
|
+
function parseUploadResult(value) {
|
|
9216
|
+
const result = value;
|
|
9217
|
+
if ((result?.status === "created" || result?.status === "exists") && typeof result.url === "string") return {
|
|
9218
|
+
status: result.status,
|
|
9219
|
+
url: result.url
|
|
9220
|
+
};
|
|
9221
|
+
throw new ShareServerError(`Unexpected share server response: ${JSON.stringify(value)}`);
|
|
9222
|
+
}
|
|
9223
|
+
/** Redirects and non-JSON answers come from something in front of the server. */
|
|
9224
|
+
async function expectShareServer(response, serverUrl) {
|
|
9225
|
+
if (response.status >= 300 && response.status < 400) {
|
|
9226
|
+
const location = response.headers.get("location");
|
|
9227
|
+
const target = location ? new URL(location, serverUrl).host : "elsewhere";
|
|
9228
|
+
throw new ShareServerError(`${new URL(serverUrl).host} redirected to ${target} instead of answering. If an auth proxy is in front of the share server, set share.headers in evalkit.config.ts.`);
|
|
9229
|
+
}
|
|
9230
|
+
const type = response.headers.get("content-type") ?? "";
|
|
9231
|
+
const denied = response.status === 401 || response.status === 403;
|
|
9232
|
+
if (denied || response.ok && !type.includes("json")) throw new ShareServerError(`${new URL(serverUrl).host} did not answer as an EvalKit share server (${response.status}${type ? `, ${type.split(";")[0]}` : ""}). ` + (denied ? "Check share.headers." : "Check share.url."));
|
|
9233
|
+
}
|
|
9234
|
+
async function shareRuns(options) {
|
|
9235
|
+
const reports = createDashboardReportReader(options.reportRoot, options.registry);
|
|
9236
|
+
return options.step("Uploading", async (progress) => {
|
|
9237
|
+
const result = {
|
|
9238
|
+
created: 0,
|
|
9239
|
+
exists: 0,
|
|
9240
|
+
failed: [],
|
|
9241
|
+
bytes: 0
|
|
9242
|
+
};
|
|
9243
|
+
for (const [index, run] of options.runs.entries()) {
|
|
9244
|
+
progress(`run ${index + 1}/${options.runs.length} · ${formatBytes(result.bytes)}`);
|
|
9245
|
+
try {
|
|
9246
|
+
const bundle = await runBundle({
|
|
9247
|
+
reports,
|
|
9248
|
+
reportRoot: options.reportRoot,
|
|
9249
|
+
registry: options.registry,
|
|
9250
|
+
run,
|
|
9251
|
+
workspaceFiles: options.workspaceFiles
|
|
9252
|
+
});
|
|
9253
|
+
const uploaded = await uploadBundle(options.serverUrl, bundle, { headers: options.headers });
|
|
9254
|
+
result.bytes += bundle.byteLength;
|
|
9255
|
+
result[uploaded.status] += 1;
|
|
9256
|
+
} catch (error) {
|
|
9257
|
+
if (error instanceof ShareServerError) throw error;
|
|
9258
|
+
result.failed.push({
|
|
9259
|
+
runId: run.id,
|
|
9260
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9261
|
+
});
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
return result;
|
|
9265
|
+
}, (result) => `${result.created} new · ${result.exists} already shared` + (result.failed.length ? ` · ${result.failed.length} failed` : "") + ` · ${formatBytes(result.bytes)}`);
|
|
9266
|
+
}
|
|
9267
|
+
function formatBytes(size) {
|
|
9268
|
+
if (size < 1024) return `${size} B`;
|
|
9269
|
+
if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} kB`;
|
|
9270
|
+
return `${(size / 1024 ** 2).toFixed(1)} MB`;
|
|
9271
|
+
}
|
|
9272
|
+
//#endregion
|
|
8906
9273
|
//#region ../cli/src/new-project-template.ts
|
|
8907
9274
|
/** Provider-free example: one fixture, agent, scorer, and a two-cell matrix. */
|
|
8908
9275
|
const starterFiles = {
|
|
@@ -9002,12 +9369,11 @@ Replace these examples with your own agent, tasks, and checks.
|
|
|
9002
9369
|
|
|
9003
9370
|
\`\`\`sh
|
|
9004
9371
|
bun install
|
|
9005
|
-
bun run check
|
|
9006
|
-
bun run
|
|
9007
|
-
bun run matrix
|
|
9008
|
-
bun run
|
|
9009
|
-
bun run
|
|
9010
|
-
bun run dashboard:build # write a read-only, shareable snapshot
|
|
9372
|
+
bun run check # type-check your eval, agent, scorer, and config
|
|
9373
|
+
bun run evalkit run # run the example across both styles
|
|
9374
|
+
bun run evalkit run --matrix styles --dry-run # inspect the matrix without running it
|
|
9375
|
+
bun run evalkit dev # inspect local reports and launch runs
|
|
9376
|
+
bun run evalkit build # write a read-only, shareable snapshot
|
|
9011
9377
|
\`\`\`
|
|
9012
9378
|
|
|
9013
9379
|
EvalKit installs from npm. Reports, trial workspaces, and dashboard snapshots stay local
|
|
@@ -9017,12 +9383,13 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
|
|
|
9017
9383
|
}
|
|
9018
9384
|
//#endregion
|
|
9019
9385
|
//#region ../cli/src/new-project.ts
|
|
9020
|
-
const npmDependency = "^0.
|
|
9386
|
+
const npmDependency = "^0.5.0";
|
|
9021
9387
|
const ignored = [
|
|
9022
9388
|
"node_modules/",
|
|
9023
9389
|
"_evalkit-results/",
|
|
9024
9390
|
"_evalkit-sandbox/",
|
|
9025
|
-
"_evalkit-dashboard/"
|
|
9391
|
+
"_evalkit-dashboard/",
|
|
9392
|
+
"_evalkit-share/"
|
|
9026
9393
|
];
|
|
9027
9394
|
const readIfExists = async (path) => {
|
|
9028
9395
|
try {
|
|
@@ -9101,11 +9468,6 @@ async function preparePackage(packagePath, name) {
|
|
|
9101
9468
|
const devDependencies = pkg.devDependencies ?? {};
|
|
9102
9469
|
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts) || !dependencies || typeof dependencies !== "object" || Array.isArray(dependencies) || !devDependencies || typeof devDependencies !== "object" || Array.isArray(devDependencies)) throw new Error(`Invalid package.json scripts/dependencies: ${packagePath}`);
|
|
9103
9470
|
scripts.check ??= "tsc --noEmit";
|
|
9104
|
-
scripts.evals ??= "evalkit run greeting --local";
|
|
9105
|
-
scripts.matrix ??= "evalkit run --matrix styles --local";
|
|
9106
|
-
scripts["matrix:plan"] ??= "evalkit run --matrix styles --dry-run";
|
|
9107
|
-
scripts.dashboard ??= "evalkit dev";
|
|
9108
|
-
scripts["dashboard:build"] ??= "evalkit build";
|
|
9109
9471
|
if (!dependencies["@leostera/evalkit"] && !devDependencies["@leostera/evalkit"]) dependencies["@leostera/evalkit"] = npmDependency;
|
|
9110
9472
|
if (!dependencies.typescript && !devDependencies.typescript) devDependencies.typescript = "^6.0.0";
|
|
9111
9473
|
if (!dependencies["@types/bun"] && !devDependencies["@types/bun"]) devDependencies["@types/bun"] = "^1.4.0";
|
|
@@ -9134,7 +9496,7 @@ async function writeStarterReadme(root, name, inPlace) {
|
|
|
9134
9496
|
}
|
|
9135
9497
|
//#endregion
|
|
9136
9498
|
//#region ../cli/src/index.ts
|
|
9137
|
-
const VERSION = "0.
|
|
9499
|
+
const VERSION = "0.5.0";
|
|
9138
9500
|
let projectRoot = process.cwd();
|
|
9139
9501
|
let reportRoot = resolve(projectRoot, "_evalkit-results");
|
|
9140
9502
|
const sandboxRoot = resolve(projectRoot, "_evalkit-sandbox");
|
|
@@ -9154,6 +9516,7 @@ async function runEvals(options) {
|
|
|
9154
9516
|
const registry = await loadRegistry();
|
|
9155
9517
|
const evaluations = options.evalIds.map((id) => registry.get(id));
|
|
9156
9518
|
if (evaluations.some((e) => !e)) throw new Error("Unknown eval");
|
|
9519
|
+
const provenance = await runProvenance(registry);
|
|
9157
9520
|
await Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
|
|
9158
9521
|
const name = evaluation.name ?? authoringId(evaluation);
|
|
9159
9522
|
const started = performance.now();
|
|
@@ -9162,10 +9525,23 @@ async function runEvals(options) {
|
|
|
9162
9525
|
workspaceRoot: sandboxRoot,
|
|
9163
9526
|
suiteId: options.suiteId,
|
|
9164
9527
|
runtime: runtimeFor(evaluation),
|
|
9165
|
-
concurrency: options.concurrency
|
|
9528
|
+
concurrency: options.concurrency,
|
|
9529
|
+
...provenance
|
|
9166
9530
|
})), Effect.tap((result) => runLog.finished(name, {}, result, performance.now() - started)), Effect.tapError((error) => Effect.sync(() => runLog.failed(name, error))));
|
|
9167
9531
|
}), { concurrency: options.concurrency })));
|
|
9168
9532
|
}
|
|
9533
|
+
/** Detected per launch: `evalkit dev` keeps running while commits change. */
|
|
9534
|
+
async function runProvenance(registry) {
|
|
9535
|
+
const source = await detectSource(projectRoot, { outputDirs: [
|
|
9536
|
+
reportRoot,
|
|
9537
|
+
sandboxRoot,
|
|
9538
|
+
resolve(projectRoot, "_evalkit-dashboard")
|
|
9539
|
+
] });
|
|
9540
|
+
return {
|
|
9541
|
+
...source ? { source } : {},
|
|
9542
|
+
catalog: runCatalog(registry)
|
|
9543
|
+
};
|
|
9544
|
+
}
|
|
9169
9545
|
function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
9170
9546
|
let matrixRunning = false;
|
|
9171
9547
|
app.get("/v1/matrix", (context) => context.json({ matrix: dashboardMatrix ? {
|
|
@@ -9210,15 +9586,16 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
|
|
|
9210
9586
|
const name = evaluation.name ?? authoringId(evaluation);
|
|
9211
9587
|
const started = performance.now();
|
|
9212
9588
|
runLog.started(name, body.parameters ?? {}, suiteId);
|
|
9213
|
-
Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
9589
|
+
runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
|
|
9214
9590
|
selection,
|
|
9215
9591
|
concurrency: 1,
|
|
9216
9592
|
trials,
|
|
9217
9593
|
...suiteId ? { suiteId } : {},
|
|
9218
9594
|
report: localReportStore(reportRoot),
|
|
9219
9595
|
workspaceRoot: sandboxRoot,
|
|
9596
|
+
...provenance,
|
|
9220
9597
|
onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
|
|
9221
|
-
}))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
9598
|
+
})))).catch((error) => runLog.failed(name, error)).finally(() => {
|
|
9222
9599
|
matrixRunning = false;
|
|
9223
9600
|
});
|
|
9224
9601
|
} else {
|
|
@@ -9413,7 +9790,7 @@ const runCommand = make$1("run", {
|
|
|
9413
9790
|
const newCommand = make$1("new", { directory: text$2({ name: "directory" }) }, ({ directory }) => Effect.tryPromise({
|
|
9414
9791
|
try: async () => {
|
|
9415
9792
|
const root = await newProject([directory]);
|
|
9416
|
-
console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run
|
|
9793
|
+
console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run evalkit run.`);
|
|
9417
9794
|
},
|
|
9418
9795
|
catch: (error) => error
|
|
9419
9796
|
}));
|
|
@@ -9505,12 +9882,88 @@ const tidyCommand = make$1("tidy", { config }, ({ config }) => Effect.tryPromise
|
|
|
9505
9882
|
},
|
|
9506
9883
|
catch: (error) => error
|
|
9507
9884
|
}));
|
|
9885
|
+
const shareServeCommand = make$1("serve", {
|
|
9886
|
+
dir: text("dir").pipe(withDefault("_evalkit-share")),
|
|
9887
|
+
port: integer("port").pipe(withDefault(4318))
|
|
9888
|
+
}, ({ dir, port }) => Effect.tryPromise({
|
|
9889
|
+
try: async () => {
|
|
9890
|
+
const root = resolve(process.cwd(), dir);
|
|
9891
|
+
const app = await createDirectoryShareApp({
|
|
9892
|
+
dir: root,
|
|
9893
|
+
dashboardRoot: dashboardAssets()
|
|
9894
|
+
});
|
|
9895
|
+
let server;
|
|
9896
|
+
try {
|
|
9897
|
+
server = Bun.serve({
|
|
9898
|
+
port,
|
|
9899
|
+
fetch: app.fetch
|
|
9900
|
+
});
|
|
9901
|
+
} catch (error) {
|
|
9902
|
+
if (error.code === "EADDRINUSE") throw new Error(`Port ${port} is already in use. Choose another: evalkit share serve --port 4319`);
|
|
9903
|
+
throw error;
|
|
9904
|
+
}
|
|
9905
|
+
banner("share serve", VERSION);
|
|
9906
|
+
console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(`http://localhost:${server.port}`)}`);
|
|
9907
|
+
console.log(` ${cyan("➜")} ${bold("Storage:".padEnd(9))}${relative(process.cwd(), root) || "."}`);
|
|
9908
|
+
console.log(`\n ${dim(`Upload with share.url = "http://localhost:${server.port}". No authentication: keep it private.`)}\n`);
|
|
9909
|
+
},
|
|
9910
|
+
catch: (error) => error
|
|
9911
|
+
}));
|
|
9912
|
+
const shareCommand = make$1("share", {
|
|
9913
|
+
config,
|
|
9914
|
+
all: boolean("all"),
|
|
9915
|
+
allowDirty: boolean("allow-dirty"),
|
|
9916
|
+
noWorkspace: boolean("no-workspace")
|
|
9917
|
+
}, ({ config, all, allowDirty, noWorkspace }) => Effect.tryPromise({
|
|
9918
|
+
try: async () => {
|
|
9919
|
+
const started = performance.now();
|
|
9920
|
+
banner("share", VERSION);
|
|
9921
|
+
await loadProjectStep(Option.getOrUndefined(config));
|
|
9922
|
+
const serverUrl = process.env.EVALKIT_SHARE_URL ?? project.config.share?.url;
|
|
9923
|
+
if (!serverUrl) throw new Error("No share server configured. Set share: { url } in evalkit.config.ts or EVALKIT_SHARE_URL.");
|
|
9924
|
+
const head = await step("Source", () => detectSource(projectRoot, { outputDirs: [
|
|
9925
|
+
reportRoot,
|
|
9926
|
+
sandboxRoot,
|
|
9927
|
+
resolve(projectRoot, "_evalkit-dashboard")
|
|
9928
|
+
] }), (source) => source ? `${repoKey(source)} @ ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : "not a git repository");
|
|
9929
|
+
if (!head) throw new Error("evalkit share needs to know the commit: run it inside a git repository or set EVALKIT_SOURCE_REPO and EVALKIT_SOURCE_COMMIT. `evalkit build` works without one.");
|
|
9930
|
+
const registry = project.registry;
|
|
9931
|
+
const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot, registry).listRuns(), head, {
|
|
9932
|
+
all,
|
|
9933
|
+
allowDirty
|
|
9934
|
+
}), (value) => selectionSummary(value, { all }));
|
|
9935
|
+
if (!selection.selected.length) throw new Error(noRunsMessage(selection, head, { all }));
|
|
9936
|
+
const result = await shareRuns({
|
|
9937
|
+
serverUrl,
|
|
9938
|
+
headers: await step("Server", async () => {
|
|
9939
|
+
const headers = await resolveShareHeaders(project.config.share?.headers);
|
|
9940
|
+
await checkShareServer(serverUrl, headers);
|
|
9941
|
+
return headers;
|
|
9942
|
+
}, (headers) => {
|
|
9943
|
+
const names = Object.keys(headers);
|
|
9944
|
+
return `${new URL(serverUrl).host}${names.length ? dim(` with ${names.join(", ")}`) : ""}`;
|
|
9945
|
+
}),
|
|
9946
|
+
reportRoot,
|
|
9947
|
+
registry,
|
|
9948
|
+
runs: selection.selected,
|
|
9949
|
+
workspaceFiles: !noWorkspace,
|
|
9950
|
+
step
|
|
9951
|
+
});
|
|
9952
|
+
for (const failure of result.failed) console.log(` ${yellow("✗")} ${failure.runId.slice(0, 8)} ${dim(failure.error)}`);
|
|
9953
|
+
const target = shareTarget(serverUrl, head, selection.selected);
|
|
9954
|
+
console.log(`\n ${bold(result.failed.length ? "Shared with errors" : "Shared")} in ${duration(performance.now() - started)}\n`);
|
|
9955
|
+
console.log(` ${cyan("➜")} ${cyan(target)}\n`);
|
|
9956
|
+
if (result.failed.length) process.exitCode = 1;
|
|
9957
|
+
},
|
|
9958
|
+
catch: (error) => error
|
|
9959
|
+
})).pipe(withSubcommands([shareServeCommand]));
|
|
9508
9960
|
const cli = run(make$1("evalkit").pipe(withSubcommands([
|
|
9509
9961
|
newCommand,
|
|
9510
9962
|
runCommand,
|
|
9511
9963
|
devCommand,
|
|
9512
9964
|
buildCommand,
|
|
9513
|
-
tidyCommand
|
|
9965
|
+
tidyCommand,
|
|
9966
|
+
shareCommand
|
|
9514
9967
|
])), {
|
|
9515
9968
|
name: "EvalKit",
|
|
9516
9969
|
version: VERSION
|