@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +8 -7
  3. package/dist/cli.mjs +473 -72
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/dashboard/assets/index-D9NtA46z.css +2 -0
  6. package/dist/dashboard/assets/{index-CVd_Np_w.js → index-DrBwLKJD.js} +1 -1
  7. package/dist/dashboard/index.html +2 -2
  8. package/dist/files-C-U0-PWW.mjs +26 -0
  9. package/dist/files-C-U0-PWW.mjs.map +1 -0
  10. package/dist/{index-M5eRlMfd.d.mts → index-U6TUTBf7.d.mts} +87 -9
  11. package/dist/index-U6TUTBf7.d.mts.map +1 -0
  12. package/dist/index-db-HdZXelOS.d.mts +142 -0
  13. package/dist/index-db-HdZXelOS.d.mts.map +1 -0
  14. package/dist/index-db-vbqEGFaU.mjs +548 -0
  15. package/dist/index-db-vbqEGFaU.mjs.map +1 -0
  16. package/dist/index.d.mts +2 -2
  17. package/dist/index.mjs +2 -2
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/runner.d.mts +21 -3
  20. package/dist/runner.d.mts.map +1 -1
  21. package/dist/runner.mjs +1 -1
  22. package/dist/share-worker.d.mts +51 -0
  23. package/dist/share-worker.d.mts.map +1 -0
  24. package/dist/share-worker.mjs +73 -0
  25. package/dist/share-worker.mjs.map +1 -0
  26. package/dist/share.d.mts +116 -0
  27. package/dist/share.d.mts.map +1 -0
  28. package/dist/share.mjs +4 -0
  29. package/dist/{src-DrwwgaPn.mjs → src-ButXPY8P.mjs} +20 -5
  30. package/dist/src-ButXPY8P.mjs.map +1 -0
  31. package/dist/{src-DluldGKn.mjs → src-DoPs9Yug.mjs} +11 -7
  32. package/dist/src-DoPs9Yug.mjs.map +1 -0
  33. package/dist/src-DzZueFn3.mjs +98 -0
  34. package/dist/src-DzZueFn3.mjs.map +1 -0
  35. package/package.json +10 -1
  36. package/dist/dashboard/assets/index-CMr4aulR.css +0 -2
  37. package/dist/index-M5eRlMfd.d.mts.map +0 -1
  38. package/dist/src-DluldGKn.mjs.map +0 -1
  39. 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 { F as defineEvalMatrix, I as canonicalParameters, m as registerEvals, n as authoringId } from "./src-DrwwgaPn.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-DluldGKn.mjs";
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"), staticIndex(template));
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 runTrials = await reports.listTrials(run.id);
8269
- await write(`runs/${run.id}/trials.json`, { trials: runTrials });
8270
- for (const trial of runTrials) {
8271
- trials += 1;
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, staticIndex(template, baseFor(route)));
8315
+ await writeFile(file, dashboardIndex(template, { base: baseFor(route) }));
8297
8316
  }
8298
- await writeFile(resolve(outDir, "404.html"), staticIndex(template, "/"));
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 one trial's JSON views and candidate files; returns the file count. */
8337
- async function snapshotTrial(reports, write, data, runId, trialId) {
8338
- const base = `runs/${runId}/trials/${trialId}`;
8339
- await write(`${base}/detail.json`, await reports.trialDetail(runId, trialId));
8340
- await write(`${base}/events.json`, { events: await reports.trajectory(runId, trialId) });
8341
- await write(`${base}/artifacts.json`, { artifacts: await reports.listArtifacts(runId, trialId) });
8342
- const workspace = await reports.listCandidateWorkspace(runId, trialId);
8343
- await write(`${base}/workspace.json`, workspace);
8344
- const root = resolve(data, base, "workspace");
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 entry of workspace) {
8347
- if (entry.kind !== "file") continue;
8348
- const target = resolve(root, entry.path);
8349
- if (!target.startsWith(`${root}${sep}`)) throw new Error(`Invalid workspace path: ${entry.path}`);
8350
- const response = await reports.candidateFile(runId, trialId, entry.path);
8351
- await mkdir(dirname(target), { recursive: true });
8352
- await writeFile(target, new Uint8Array(await response.arrayBuffer()));
8353
- files += 1;
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 files;
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
- if (!values.json) console.log(`Running ${cells} cell(s) from ${plan.matrix}`);
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
- report: localReportStore(resolve(project.root, project.config.reportDir ?? "_evalkit-results")),
8835
- workspaceRoot: resolve(project.root, project.config.sandboxDir ?? "_evalkit-sandbox"),
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,161 @@ 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 the bundle (4xx): retrying the same bundle cannot help. */
9156
+ var UploadRejected = class extends Error {};
9157
+ /** POST one bundle, retrying network errors and 5xx responses. */
9158
+ async function uploadBundle(serverUrl, bundle, options = {}) {
9159
+ const attempts = options.attempts ?? 3;
9160
+ let lastError;
9161
+ for (let attempt = 1; attempt <= attempts; attempt++) {
9162
+ try {
9163
+ return await postBundle(serverUrl, bundle);
9164
+ } catch (error) {
9165
+ if (error instanceof UploadRejected) throw error;
9166
+ lastError = error;
9167
+ }
9168
+ if (attempt < attempts) await Bun.sleep((options.delayMs ?? 500) * 2 ** (attempt - 1));
9169
+ }
9170
+ throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Upload failed: ${String(lastError)}`);
9171
+ }
9172
+ async function postBundle(serverUrl, bundle) {
9173
+ const response = await fetch(new URL("/api/v1/uploads", serverUrl), {
9174
+ method: "POST",
9175
+ headers: { "content-type": "application/octet-stream" },
9176
+ body: bundle
9177
+ });
9178
+ if (response.ok) return parseUploadResult(await response.json());
9179
+ const reason = await response.text();
9180
+ if (response.status < 500) throw new UploadRejected(`Share server rejected the upload: ${reason}`);
9181
+ throw new Error(`Share server error ${response.status}: ${reason}`);
9182
+ }
9183
+ function parseUploadResult(value) {
9184
+ const result = value;
9185
+ if ((result?.status === "created" || result?.status === "exists") && typeof result.url === "string") return {
9186
+ status: result.status,
9187
+ url: result.url
9188
+ };
9189
+ throw new UploadRejected(`Unexpected share server response: ${JSON.stringify(value)}`);
9190
+ }
9191
+ async function shareRuns(options) {
9192
+ const reports = createDashboardReportReader(options.reportRoot, options.registry);
9193
+ return options.step("Uploading", async (progress) => {
9194
+ const result = {
9195
+ created: 0,
9196
+ exists: 0,
9197
+ failed: [],
9198
+ bytes: 0
9199
+ };
9200
+ for (const [index, run] of options.runs.entries()) {
9201
+ progress(`run ${index + 1}/${options.runs.length} · ${formatBytes(result.bytes)}`);
9202
+ try {
9203
+ const bundle = await runBundle({
9204
+ reports,
9205
+ reportRoot: options.reportRoot,
9206
+ registry: options.registry,
9207
+ run,
9208
+ workspaceFiles: options.workspaceFiles
9209
+ });
9210
+ const uploaded = await uploadBundle(options.serverUrl, bundle);
9211
+ result.bytes += bundle.byteLength;
9212
+ result[uploaded.status] += 1;
9213
+ } catch (error) {
9214
+ result.failed.push({
9215
+ runId: run.id,
9216
+ error: error instanceof Error ? error.message : String(error)
9217
+ });
9218
+ }
9219
+ }
9220
+ return result;
9221
+ }, (result) => `${result.created} new · ${result.exists} already shared` + (result.failed.length ? ` · ${result.failed.length} failed` : "") + ` · ${formatBytes(result.bytes)}`);
9222
+ }
9223
+ function formatBytes(size) {
9224
+ if (size < 1024) return `${size} B`;
9225
+ if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} kB`;
9226
+ return `${(size / 1024 ** 2).toFixed(1)} MB`;
9227
+ }
9228
+ //#endregion
8906
9229
  //#region ../cli/src/new-project-template.ts
8907
9230
  /** Provider-free example: one fixture, agent, scorer, and a two-cell matrix. */
8908
9231
  const starterFiles = {
@@ -9002,12 +9325,11 @@ Replace these examples with your own agent, tasks, and checks.
9002
9325
 
9003
9326
  \`\`\`sh
9004
9327
  bun install
9005
- bun run check # type-check your eval, agent, scorer, and config
9006
- bun run evals # run the example across both styles
9007
- bun run matrix:plan # inspect the matrix without running it
9008
- bun run matrix # run the configured matrix
9009
- bun run dashboard # inspect local reports and launch runs
9010
- bun run dashboard:build # write a read-only, shareable snapshot
9328
+ bun run check # type-check your eval, agent, scorer, and config
9329
+ bun run evalkit run # run the example across both styles
9330
+ bun run evalkit run --matrix styles --dry-run # inspect the matrix without running it
9331
+ bun run evalkit dev # inspect local reports and launch runs
9332
+ bun run evalkit build # write a read-only, shareable snapshot
9011
9333
  \`\`\`
9012
9334
 
9013
9335
  EvalKit installs from npm. Reports, trial workspaces, and dashboard snapshots stay local
@@ -9017,12 +9339,13 @@ scores, evidence, and candidate files (never evaluator workspaces): review it be
9017
9339
  }
9018
9340
  //#endregion
9019
9341
  //#region ../cli/src/new-project.ts
9020
- const npmDependency = "^0.3.0";
9342
+ const npmDependency = "^0.4.0";
9021
9343
  const ignored = [
9022
9344
  "node_modules/",
9023
9345
  "_evalkit-results/",
9024
9346
  "_evalkit-sandbox/",
9025
- "_evalkit-dashboard/"
9347
+ "_evalkit-dashboard/",
9348
+ "_evalkit-share/"
9026
9349
  ];
9027
9350
  const readIfExists = async (path) => {
9028
9351
  try {
@@ -9101,11 +9424,6 @@ async function preparePackage(packagePath, name) {
9101
9424
  const devDependencies = pkg.devDependencies ?? {};
9102
9425
  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
9426
  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
9427
  if (!dependencies["@leostera/evalkit"] && !devDependencies["@leostera/evalkit"]) dependencies["@leostera/evalkit"] = npmDependency;
9110
9428
  if (!dependencies.typescript && !devDependencies.typescript) devDependencies.typescript = "^6.0.0";
9111
9429
  if (!dependencies["@types/bun"] && !devDependencies["@types/bun"]) devDependencies["@types/bun"] = "^1.4.0";
@@ -9134,7 +9452,7 @@ async function writeStarterReadme(root, name, inPlace) {
9134
9452
  }
9135
9453
  //#endregion
9136
9454
  //#region ../cli/src/index.ts
9137
- const VERSION = "0.3.0";
9455
+ const VERSION = "0.4.0";
9138
9456
  let projectRoot = process.cwd();
9139
9457
  let reportRoot = resolve(projectRoot, "_evalkit-results");
9140
9458
  const sandboxRoot = resolve(projectRoot, "_evalkit-sandbox");
@@ -9154,6 +9472,7 @@ async function runEvals(options) {
9154
9472
  const registry = await loadRegistry();
9155
9473
  const evaluations = options.evalIds.map((id) => registry.get(id));
9156
9474
  if (evaluations.some((e) => !e)) throw new Error("Unknown eval");
9475
+ const provenance = await runProvenance(registry);
9157
9476
  await Effect.runPromise(runnerLogs(Effect.all(evaluations.map((evaluation) => {
9158
9477
  const name = evaluation.name ?? authoringId(evaluation);
9159
9478
  const started = performance.now();
@@ -9162,10 +9481,23 @@ async function runEvals(options) {
9162
9481
  workspaceRoot: sandboxRoot,
9163
9482
  suiteId: options.suiteId,
9164
9483
  runtime: runtimeFor(evaluation),
9165
- concurrency: options.concurrency
9484
+ concurrency: options.concurrency,
9485
+ ...provenance
9166
9486
  })), Effect.tap((result) => runLog.finished(name, {}, result, performance.now() - started)), Effect.tapError((error) => Effect.sync(() => runLog.failed(name, error))));
9167
9487
  }), { concurrency: options.concurrency })));
9168
9488
  }
9489
+ /** Detected per launch: `evalkit dev` keeps running while commits change. */
9490
+ async function runProvenance(registry) {
9491
+ const source = await detectSource(projectRoot, { outputDirs: [
9492
+ reportRoot,
9493
+ sandboxRoot,
9494
+ resolve(projectRoot, "_evalkit-dashboard")
9495
+ ] });
9496
+ return {
9497
+ ...source ? { source } : {},
9498
+ catalog: runCatalog(registry)
9499
+ };
9500
+ }
9169
9501
  function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
9170
9502
  let matrixRunning = false;
9171
9503
  app.get("/v1/matrix", (context) => context.json({ matrix: dashboardMatrix ? {
@@ -9210,15 +9542,16 @@ function registerMatrixRoutes(app, registry, dashboardMatrix, trials) {
9210
9542
  const name = evaluation.name ?? authoringId(evaluation);
9211
9543
  const started = performance.now();
9212
9544
  runLog.started(name, body.parameters ?? {}, suiteId);
9213
- Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
9545
+ runProvenance(registry).then((provenance) => Effect.runPromise(runnerLogs(runMatrix(dashboardMatrix, {
9214
9546
  selection,
9215
9547
  concurrency: 1,
9216
9548
  trials,
9217
9549
  ...suiteId ? { suiteId } : {},
9218
9550
  report: localReportStore(reportRoot),
9219
9551
  workspaceRoot: sandboxRoot,
9552
+ ...provenance,
9220
9553
  onResult: (cell, result) => runLog.cell(cell, result, performance.now() - started)
9221
- }))).catch((error) => runLog.failed(name, error)).finally(() => {
9554
+ })))).catch((error) => runLog.failed(name, error)).finally(() => {
9222
9555
  matrixRunning = false;
9223
9556
  });
9224
9557
  } else {
@@ -9413,7 +9746,7 @@ const runCommand = make$1("run", {
9413
9746
  const newCommand = make$1("new", { directory: text$2({ name: "directory" }) }, ({ directory }) => Effect.tryPromise({
9414
9747
  try: async () => {
9415
9748
  const root = await newProject([directory]);
9416
- console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run evals.`);
9749
+ console.log(`Created EvalKit project at ${root}\nRun cd ${root} && bun install && bun run evalkit run.`);
9417
9750
  },
9418
9751
  catch: (error) => error
9419
9752
  }));
@@ -9505,12 +9838,80 @@ const tidyCommand = make$1("tidy", { config }, ({ config }) => Effect.tryPromise
9505
9838
  },
9506
9839
  catch: (error) => error
9507
9840
  }));
9841
+ const shareServeCommand = make$1("serve", {
9842
+ dir: text("dir").pipe(withDefault("_evalkit-share")),
9843
+ port: integer("port").pipe(withDefault(4318))
9844
+ }, ({ dir, port }) => Effect.tryPromise({
9845
+ try: async () => {
9846
+ const root = resolve(process.cwd(), dir);
9847
+ const app = await createDirectoryShareApp({
9848
+ dir: root,
9849
+ dashboardRoot: dashboardAssets()
9850
+ });
9851
+ let server;
9852
+ try {
9853
+ server = Bun.serve({
9854
+ port,
9855
+ fetch: app.fetch
9856
+ });
9857
+ } catch (error) {
9858
+ if (error.code === "EADDRINUSE") throw new Error(`Port ${port} is already in use. Choose another: evalkit share serve --port 4319`);
9859
+ throw error;
9860
+ }
9861
+ banner("share serve", VERSION);
9862
+ console.log(` ${cyan("➜")} ${bold("Local:".padEnd(9))}${cyan(`http://localhost:${server.port}`)}`);
9863
+ console.log(` ${cyan("➜")} ${bold("Storage:".padEnd(9))}${relative(process.cwd(), root) || "."}`);
9864
+ console.log(`\n ${dim(`Upload with share.url = "http://localhost:${server.port}". No authentication: keep it private.`)}\n`);
9865
+ },
9866
+ catch: (error) => error
9867
+ }));
9868
+ const shareCommand = make$1("share", {
9869
+ config,
9870
+ all: boolean("all"),
9871
+ allowDirty: boolean("allow-dirty"),
9872
+ noWorkspace: boolean("no-workspace")
9873
+ }, ({ config, all, allowDirty, noWorkspace }) => Effect.tryPromise({
9874
+ try: async () => {
9875
+ const started = performance.now();
9876
+ banner("share", VERSION);
9877
+ await loadProjectStep(Option.getOrUndefined(config));
9878
+ const serverUrl = process.env.EVALKIT_SHARE_URL ?? project.config.share?.url;
9879
+ if (!serverUrl) throw new Error("No share server configured. Set share: { url } in evalkit.config.ts or EVALKIT_SHARE_URL.");
9880
+ const head = await step("Source", () => detectSource(projectRoot, { outputDirs: [
9881
+ reportRoot,
9882
+ sandboxRoot,
9883
+ resolve(projectRoot, "_evalkit-dashboard")
9884
+ ] }), (source) => source ? `${repoKey(source)} @ ${shortSource(source)}${source.ref ? ` (${source.ref})` : ""}` : "not a git repository");
9885
+ 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.");
9886
+ const registry = project.registry;
9887
+ const selection = await step("Runs", async () => selectRuns(await createDashboardReportReader(reportRoot, registry).listRuns(), head, {
9888
+ all,
9889
+ allowDirty
9890
+ }), (value) => selectionSummary(value, { all }));
9891
+ if (!selection.selected.length) throw new Error(noRunsMessage(selection, head, { all }));
9892
+ const result = await shareRuns({
9893
+ serverUrl,
9894
+ reportRoot,
9895
+ registry,
9896
+ runs: selection.selected,
9897
+ workspaceFiles: !noWorkspace,
9898
+ step
9899
+ });
9900
+ for (const failure of result.failed) console.log(` ${yellow("✗")} ${failure.runId.slice(0, 8)} ${dim(failure.error)}`);
9901
+ const target = shareTarget(serverUrl, head, selection.selected);
9902
+ console.log(`\n ${bold(result.failed.length ? "Shared with errors" : "Shared")} in ${duration(performance.now() - started)}\n`);
9903
+ console.log(` ${cyan("➜")} ${cyan(target)}\n`);
9904
+ if (result.failed.length) process.exitCode = 1;
9905
+ },
9906
+ catch: (error) => error
9907
+ })).pipe(withSubcommands([shareServeCommand]));
9508
9908
  const cli = run(make$1("evalkit").pipe(withSubcommands([
9509
9909
  newCommand,
9510
9910
  runCommand,
9511
9911
  devCommand,
9512
9912
  buildCommand,
9513
- tidyCommand
9913
+ tidyCommand,
9914
+ shareCommand
9514
9915
  ])), {
9515
9916
  name: "EvalKit",
9516
9917
  version: VERSION