evals-lab 0.14.0 → 0.16.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 CHANGED
@@ -5,6 +5,103 @@ Newest first. Read a release's **Upgrade notes** before installing it: an
5
5
  upgrade can rewrite what the lab keeps in your data directory, and an older
6
6
  version cannot always read it back.
7
7
 
8
+ ## 0.16.0
9
+
10
+ ### Upgrade notes
11
+
12
+ - **Copy your data directory before upgrading** (see "User data" in the
13
+ README). An experiment 0.16.0 saves is pipeline version 20, which 0.15.x
14
+ cannot read. To go back, reinstall 0.15.0 and restore the copy.
15
+ - An experiment's jobs and targets become one Steps list in run order (Get
16
+ items, Request, Read response, Filter), with Variants beside it. Every
17
+ experiment runs and grades as it did.
18
+ - The lab no longer ships presets of its own: presets come from a pack.
19
+
20
+ ### Added
21
+
22
+ - The Experiment page is a Steps list: Add step, Move up and down, Clone and
23
+ Remove, and a step out of order says where it goes.
24
+ - A Variants table under the steps: each Variant reads (Same) or holds an
25
+ override, its prompt included. Add variants from presets fills it from a
26
+ pack's presets.
27
+ - An experiment can have several Get items steps; Results groups rows by
28
+ Source.
29
+ - A flow's Request chooses who answers it: the Request, its Recorded
30
+ answer, or Echo.
31
+ - Score with latest Eval: Results shows how kept responses grade against the
32
+ Eval as it reads now, without asking a model.
33
+ - Add eval case on a Results row that has none.
34
+ - History opens on Overview: an experiment's variants over its recent runs,
35
+ Previous performance with the best marked, New failures and New passing,
36
+ and an Items matrix.
37
+ - Guides on Home, replacing Setup › Wizards. Old links still land.
38
+ - `evals-lab presets compare` sets an app's decoding settings beside a lab's
39
+ or a pack's presets.
40
+
41
+ ### Changed
42
+
43
+ - On screen: Experiments, Eval, Eval Cases, Variant, Item and Response.
44
+ Nothing stored is renamed.
45
+ - Setup calls an AI connection only when a button is pressed. Model is
46
+ listed as its picker opens.
47
+ - Errors and statuses read the same everywhere, with field errors on the
48
+ field.
49
+ - Empty values read as a short parenthetical, such as (Not set).
50
+ - Runs are named by when they ran.
51
+ - History, Library and Setup reopen the section last used.
52
+ - The Evals Lab title links to Home.
53
+
54
+ ### Fixed
55
+
56
+ - An open Request fits a phone's width.
57
+ - History's experiment filter lists each experiment once.
58
+ - Items strips keep their width in Firefox.
59
+ - An experiment opened unchanged is no longer saved again.
60
+
61
+ ## 0.15.0
62
+
63
+ ### Upgrade notes
64
+
65
+ - **Copy your data directory before upgrading** (see "User data" in the
66
+ README). A pipeline 0.15.0 saves is pipeline version 19 and a filter set is
67
+ version 4, neither of which 0.14.x can read. To go back, reinstall 0.14.0
68
+ and restore the copy.
69
+ - A job's Pre-format and Post-format filters become one Filters list, in run
70
+ order around its Response Format, and each filter becomes one of seven
71
+ verbs. Every reply reads as it did, except a filter that was linked where
72
+ it never ran: it now runs where it sits.
73
+
74
+ ### Added
75
+
76
+ - Filters are seven verbs, in one Filters table with the Response Format as
77
+ a row.
78
+ - A typed prompt edited between runs becomes its next version, not a new
79
+ prompt.
80
+ - Results show a spinner on the row a run is processing, and (Not run) on
81
+ rows a stopped run never reached.
82
+ - Enter in a picker's search picks the top match.
83
+
84
+ ### Changed
85
+
86
+ - A picker ends in Cancel and a viewer in Close.
87
+ - Escape closes the innermost dialog or menu, and focus returns to what
88
+ opened it.
89
+ - Enter presses the main button in more dialogs.
90
+ - Links are amber.
91
+ - Keyboard and screen readers: focus rings are no longer clipped, menus
92
+ move with the arrow keys, and Results' run tabs are a proper tab list.
93
+ - When another tab saved first, the conflict bar keeps that copy by default,
94
+ with one button to override it.
95
+ - A dynamic Source's Status is its newest fetch's, and its button says Get
96
+ response.
97
+ - The Sources toolbar stays on one row on a phone, and a tile's quota says
98
+ what it measures.
99
+
100
+ ### Fixed
101
+
102
+ - Add source adds one Source, not two.
103
+ - A focus trap skips disabled buttons.
104
+
8
105
  ## 0.14.0
9
106
 
10
107
  ### Upgrade notes
package/README.md CHANGED
@@ -4,10 +4,10 @@ Test what a model, an HTTP endpoint or an API answers, side by side, and score
4
4
  it with evals you define in advance. Runs locally in a friendly UI, and
5
5
  doesn't send your data anywhere you haven't pointed it.
6
6
 
7
- A pipeline takes items from a **Source** and sends each one to one or more
8
- **Targets**, then scores the **Responses**.
7
+ An experiment takes items from a **Source** and sends each one to one or more
8
+ **Variants**, then scores the **Responses**.
9
9
 
10
- - **Targets** can be models, local (Ollama, llama.cpp) or hosted (Anthropic,
10
+ - **Variants** can be models, local (Ollama, llama.cpp) or hosted (Anthropic,
11
11
  OpenAI, any compatible API), or **any HTTP endpoint**: your own service,
12
12
  an internal API, a gateway in front of a model. The lab sends each request
13
13
  as you write it, with the key held in a connection rather than in the
@@ -18,8 +18,8 @@ A pipeline takes items from a **Source** and sends each one to one or more
18
18
  a fresh one at run time.
19
19
 
20
20
  Evals include parsing checks, item counts, comparison with production
21
- replies, grading by a model, and eval groups (datasets) shared between
22
- pipelines. Run history is kept and can be exported.
21
+ responses, grading by a model, and evals (evals) shared between
22
+ experiments. Run history is kept and can be exported.
23
23
 
24
24
  The same evals run in CI: `evals-lab run` fails the build when an eval
25
25
  fails, like a failing unit test.
@@ -33,7 +33,7 @@ Example uses:
33
33
  keywords it returns
34
34
  - Comparing two models, or two prompts, on the same items and seeing which
35
35
  scores higher
36
- - Grading what an API returns against a dataset of expected answers
36
+ - Grading what an API returns against an eval of expected answers
37
37
  - Checking which model can meet the evals in a Power Automate workflow
38
38
  - Sending images to a model with vision and comparing its answers for
39
39
  accuracy
@@ -45,8 +45,8 @@ npm install -g evals-lab
45
45
  evals-lab
46
46
  ```
47
47
 
48
- Open <http://localhost:8080>. A new lab starts with six demos, each a dataset
49
- and its pipeline, which run once you add an AI Connection in Setup. Note the first start takes longer while it installs them. Without
48
+ Open <http://localhost:8080>. A new lab starts with six demos, each an eval
49
+ and its experiment, which run once you add an AI Connection in Setup. Note the first start takes longer while it installs them. Without
50
50
  installing: `npx evals-lab`.
51
51
 
52
52
  | Needs | Windows | Linux |
@@ -89,7 +89,7 @@ evals-lab [--port <n>] [--host <address>] [--data-dir <dir>]
89
89
 
90
90
  ## In CI
91
91
 
92
- A pipeline can run with no lab, from a repository. In the lab, the pipeline's
92
+ An experiment can run with no lab, from a repository. In the lab, the experiment's
93
93
  menu › **Export for CI…** downloads it as a bundle: unzip it into the
94
94
  repository, then:
95
95
 
@@ -104,7 +104,7 @@ evals-lab run <bundle-dir | pipeline.yaml> [--items <dir>] [--json <file|->]
104
104
 
105
105
  | Option | |
106
106
  |---|---|
107
- | `--items` | the items the pipeline's Source reads; default the bundle's `items/` |
107
+ | `--items` | the items the experiment's Source reads; default the bundle's `items/` |
108
108
  | `--json` | the JSON report; `-` for stdout |
109
109
  | `--junit` | JUnit XML, for a CI test panel |
110
110
  | `--summary` | the results as Markdown, added to the file: for GitHub, `"$GITHUB_STEP_SUMMARY"` |
@@ -124,7 +124,7 @@ goes to stderr.
124
124
  | `2` | incomplete: an item is missing, or did not run |
125
125
  | `3` | broken or refused |
126
126
 
127
- An item a case names that is not in the items is reported as Missing.
127
+ An item an eval case names that is not in the items is reported as Missing.
128
128
  Images need ImageMagick, as in the lab.
129
129
 
130
130
  ### GitHub Actions
@@ -160,7 +160,7 @@ CI systems run the same command and read `--junit` in their test panel.
160
160
 
161
161
  ### Against a running lab
162
162
 
163
- CI can run a lab's own pipeline there instead, so the run shows in that lab's
163
+ CI can run a lab's own experiment there instead, so the run shows in that lab's
164
164
  History and its keys never leave it:
165
165
 
166
166
  ```bash
@@ -175,13 +175,34 @@ evals-lab run --lab <url> --pipeline <name> [--wait] [--json <file|->]
175
175
  | Option | |
176
176
  |---|---|
177
177
  | `--lab` | the lab's address; `LAB_PASSWORD` is sent when it is set |
178
- | `--pipeline` | the pipeline, by its name or its id |
178
+ | `--pipeline` | the experiment, by its name or its id |
179
179
  | `--wait` | wait for the run, and exit with the lab's verdict on it |
180
180
 
181
181
  Without `--wait` the run's id is printed once it is queued, and the exit code
182
182
  is `0`; `--json`, `--junit`, `--summary`, `--min-pass` and `--progress` need
183
183
  `--wait`. The exit codes are the ones above, and a cancelled run is `2`.
184
184
 
185
+ ### Presets
186
+
187
+ An app that sends decoding settings of its own can compare them with the
188
+ presets its pack gives the lab, as the request fields each sends:
189
+
190
+ ```bash
191
+ evals-lab presets compare app-presets.json --pack my-app.labpack [--fail-on-difference]
192
+ evals-lab presets compare app-presets.json --lab https://lab.example
193
+ ```
194
+
195
+ It reports each preset as the same, different or on one side only, and exits
196
+ `0`; `--fail-on-difference` exits `1` on a difference. The app's file names
197
+ each preset's request fields:
198
+
199
+ ```json
200
+ { "format": "evals-lab/presets", "version": 1,
201
+ "presets": [{ "id": "repeat-1.2", "request": { "repeat_penalty": 1.2 } }] }
202
+ ```
203
+
204
+ `evals-lab presets --help` has the rest.
205
+
185
206
  ## Getting started
186
207
 
187
208
  First set up a Setup profile: **Setup › Add profile**, then pick its type —
@@ -190,7 +211,7 @@ endpoint — and give its address, model and key. NB: An Ollama profile without
190
211
 
191
212
  Set up your data source(s) in Library.
192
213
 
193
- Then create a Pipeline to Run. Changes are auto-saved. Use the "Restore" menu to go back and undo any changes you don't want to keep.
214
+ Then create an Experiment to Run. Changes are auto-saved. Use the "Restore" menu to go back and undo any changes you don't want to keep.
194
215
 
195
216
  ## Power Automate
196
217
 
package/bin/evals-lab.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // The npm package's launcher: `evals-lab` starts the lab's server and says
3
3
  // where it is, and `evals-lab run <bundle>` runs an Export for CI bundle with
4
4
  // no server at all, or `evals-lab run --lab <url>` a running lab's pipeline
5
- // (run.js).
5
+ // (run.js). `evals-lab presets compare` sets an app's decoding settings
6
+ // beside a lab's or a pack's presets (presets.js).
6
7
  //
7
8
  // The lab itself is server.py, a stdlib Python server, staged beside this file
8
9
  // in ../lab by build.js. What this adds is what a lab run by someone who only
@@ -32,6 +33,8 @@ const USAGE = `Usage: evals-lab [options]
32
33
  evals-lab run --lab <url> --pipeline <name> [--wait] [options]
33
34
  evals-lab export --lab <url> [--workspace <slug>] [--out <file>]
34
35
  evals-lab import <file> --lab <url> (evals-lab import --help)
36
+ evals-lab presets compare <file> (--lab <url> | --pack <file>)
37
+ (evals-lab presets --help)
35
38
 
36
39
  --port <n> the port to listen on. Default: $PORT, else 8080.
37
40
  --host <address> the address to listen on. Default: $LAB_HOST, else
@@ -167,6 +170,10 @@ async function main() {
167
170
  process.exitCode = await require("./lab-file.js").labFile(process.argv[2], process.argv.slice(3));
168
171
  return;
169
172
  }
173
+ if (process.argv[2] === "presets") {
174
+ process.exitCode = await require("./presets.js").presets(process.argv.slice(3), { lab: LAB });
175
+ return;
176
+ }
170
177
  if (process.argv[2] === "run") {
171
178
  process.exitCode = await require("./run.js").run(process.argv.slice(3), { lab: LAB });
172
179
  return;
package/bin/presets.js ADDED
@@ -0,0 +1,234 @@
1
+ // `evals-lab presets compare <file>` (#652, docs/presets.md): an app's
2
+ // decoding settings beside the presets a lab or a pack offers, compared as
3
+ // what each one sends.
4
+ //
5
+ // WHAT A PRESET SENDS. A preset is written in the lab's spelling
6
+ // (repeatPenalty: "1.2") and an app's in whatever its own code says, so the
7
+ // two are compared where they meet: the request fields. A lab preset's are
8
+ // the fields its type's request carries with the preset applied and not
9
+ // without it -- the core builds both, so this is what a run would send. The
10
+ // app's file names its fields directly.
11
+ //
12
+ // A COMPARISON, NOT AN ASSERTION. It says what is the same, what differs and
13
+ // what is only on one side, and exits 0; --fail-on-difference exits 1 when
14
+ // anything is not the same, for a CI that wants that. 3 is a comparison
15
+ // refused: a file that is not one, a lab that did not answer.
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const zlib = require("zlib");
19
+ const { labApi, Refused } = require("./run.js");
20
+
21
+ const EXIT = { same: 0, different: 1, broken: 3 };
22
+ const FORMAT = "evals-lab/presets";
23
+
24
+ const USAGE = `Usage: evals-lab presets compare <file> (--lab <url> | --pack <file>) [options]
25
+
26
+ <file> the app's presets: { "format": "${FORMAT}", "version": 1,
27
+ "presets": [{ "id", "type"?, "request": { field: value } }] }
28
+ --lab <url> compare with a running lab's presets. LAB_PASSWORD is sent
29
+ when it is set.
30
+ --workspace <slug> the lab's workspace, by its /w/<slug> address. Default:
31
+ the lab's default workspace.
32
+ --pack <file> compare with a pack's presets: a .labpack, or its
33
+ presets.json.
34
+ --json <file|-> write the comparison as JSON. "-" means stdout.
35
+ --fail-on-difference
36
+ exit 1 when anything differs or is on one side only.
37
+ --help this.
38
+
39
+ Exit codes: 0 compared, 1 a difference with --fail-on-difference, 3 refused.`;
40
+
41
+ function options(argv) {
42
+ const o = { file: null, lab: null, workspace: null, pack: null, json: null, failOnDifference: false };
43
+ const value = { "--lab": "lab", "--workspace": "workspace", "--pack": "pack", "--json": "json" };
44
+ const [verb, ...rest] = argv;
45
+ if (verb === "--help" || verb === "-h" || verb === undefined) return { help: true };
46
+ if (verb !== "compare") throw new Refused(`no presets command ${verb}`, true);
47
+ for (let i = 0; i < rest.length; i++) {
48
+ const a = rest[i];
49
+ if (a === "--help" || a === "-h") return { help: true };
50
+ if (a === "--fail-on-difference") o.failOnDifference = true;
51
+ else if (value[a]) {
52
+ if (rest[i + 1] === undefined) throw new Refused(`${a} needs a value`, true);
53
+ o[value[a]] = rest[++i];
54
+ } else if (a.startsWith("-") && a !== "-") throw new Refused(`no option ${a}`, true);
55
+ else if (o.file === null) o.file = a;
56
+ else throw new Refused(`one file to compare, not ${a} as well`, true);
57
+ }
58
+ if (o.file === null) throw new Refused("name the app's presets file", true);
59
+ if ((o.lab === null) === (o.pack === null)) throw new Refused("compare with one of --lab or --pack", true);
60
+ if (o.lab !== null && !/^https?:\/\/[^/]/i.test(o.lab)) throw new Refused(`--lab ${o.lab} is not an http or https address`, true);
61
+ if (o.workspace !== null && o.lab === null) throw new Refused("--workspace is --lab's", true);
62
+ return o;
63
+ }
64
+
65
+ // The entries of a zip, by name, as Buffers. A pack is a plain zip, stored
66
+ // or deflated, so this reads the central directory and nothing fancier.
67
+ function unzip(buf) {
68
+ let end = buf.length - 22;
69
+ while (end >= 0 && buf.readUInt32LE(end) !== 0x06054b50) end--;
70
+ if (end < 0) throw new Error("not a zip");
71
+ const count = buf.readUInt16LE(end + 10);
72
+ let at = buf.readUInt32LE(end + 16);
73
+ const files = {};
74
+ for (let n = 0; n < count; n++) {
75
+ if (buf.readUInt32LE(at) !== 0x02014b50) throw new Error("a broken zip directory");
76
+ const method = buf.readUInt16LE(at + 10), size = buf.readUInt32LE(at + 20);
77
+ const nameLen = buf.readUInt16LE(at + 28), extraLen = buf.readUInt16LE(at + 30), noteLen = buf.readUInt16LE(at + 32);
78
+ const local = buf.readUInt32LE(at + 42);
79
+ const name = buf.toString("utf8", at + 46, at + 46 + nameLen);
80
+ const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
81
+ const raw = buf.subarray(start, start + size);
82
+ if (method === 0) files[name] = raw;
83
+ else if (method === 8) files[name] = zlib.inflateRawSync(raw);
84
+ at += 46 + nameLen + extraLen + noteLen;
85
+ }
86
+ return files;
87
+ }
88
+
89
+ // A pack's presets: from its .labpack, or from its presets.json as it is.
90
+ function packPresets(file) {
91
+ let buf;
92
+ try { buf = fs.readFileSync(path.resolve(file)); }
93
+ catch (e) { throw new Refused(`${file}: ${e.message}`); }
94
+ let text = buf.toString("utf8");
95
+ if (buf.readUInt32LE(0) === 0x04034b50) {
96
+ let entries;
97
+ try { entries = unzip(buf); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
98
+ if (!entries["presets.json"]) return [];
99
+ text = entries["presets.json"].toString("utf8");
100
+ }
101
+ let list;
102
+ try { list = JSON.parse(text); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
103
+ if (!Array.isArray(list) || !list.every(p => p && typeof p.id === "string" && p.opts && typeof p.opts === "object")) {
104
+ throw new Refused(`${file}: a pack's presets are a list of { id, label, type?, opts }`);
105
+ }
106
+ return list;
107
+ }
108
+
109
+ // The app's file, or a Refused saying what is wrong with it.
110
+ function appPresets(file, text) {
111
+ let doc;
112
+ try { doc = JSON.parse(text); } catch (e) { throw new Refused(`${file}: ${e.message}`); }
113
+ if (doc?.format !== FORMAT) throw new Refused(`${file} is not an ${FORMAT} file`);
114
+ if (doc.version !== 1) throw new Refused(`${file} is ${FORMAT} version ${doc.version}, and this reads 1`);
115
+ const list = doc.presets;
116
+ if (!Array.isArray(list) || !list.every(p => p && typeof p.id === "string"
117
+ && p.request && typeof p.request === "object" && !Array.isArray(p.request))) {
118
+ throw new Refused(`${file}: presets are a list of { id, type?, request }`);
119
+ }
120
+ return list;
121
+ }
122
+
123
+ const typeOf = p => p.type ?? "llama.cpp";
124
+
125
+ // The request fields a lab [preset] sends: what its type's request carries
126
+ // with it applied and not without.
127
+ function presetRequest(core, preset) {
128
+ const conn = { type: typeOf(preset), model: "m", url: "" };
129
+ const body = c => {
130
+ const b = { ...core.connectionRequest(c, "p", null, "").body };
131
+ delete b.messages;
132
+ return b;
133
+ };
134
+ const base = body(conn), sent = body({ ...conn, ...preset.opts });
135
+ return Object.fromEntries(Object.entries(sent).filter(([k, v]) => JSON.stringify(base[k]) !== JSON.stringify(v)));
136
+ }
137
+
138
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
139
+
140
+ function unique(list, where) {
141
+ const ids = list.map(p => p.id);
142
+ const twice = ids.find((id, n) => ids.indexOf(id) !== n);
143
+ if (twice !== undefined) throw new Refused(`${where} has two presets named ${JSON.stringify(twice)}`);
144
+ }
145
+
146
+ /**
147
+ * The comparison of the lab's [lab] presets (its spelling) with the app's
148
+ * [app] ones (request fields): a row per preset id, the lab's in their
149
+ * order and then the app's only, each with its fields.
150
+ */
151
+ function compare(core, lab, app) {
152
+ unique(lab, "the lab"); unique(app, "the file");
153
+ const theirs = new Map(app.map(p => [p.id, p]));
154
+ const rows = [];
155
+ for (const p of lab) {
156
+ let sent;
157
+ try { sent = presetRequest(core, p); }
158
+ catch (e) { throw new Refused(`the lab's preset ${p.id}: ${e.message}`); }
159
+ const other = theirs.get(p.id);
160
+ if (!other) { rows.push({ id: p.id, status: "lab only", lab: sent }); continue; }
161
+ const fields = [];
162
+ if (typeOf(p) !== typeOf(other)) fields.push({ field: "(type)", status: "different", lab: typeOf(p), file: typeOf(other) });
163
+ for (const k of [...new Set([...Object.keys(sent), ...Object.keys(other.request)])].sort()) {
164
+ if (!(k in other.request)) fields.push({ field: k, status: "lab only", lab: sent[k] });
165
+ else if (!(k in sent)) fields.push({ field: k, status: "file only", file: other.request[k] });
166
+ else fields.push({ field: k, status: same(sent[k], other.request[k]) ? "same" : "different", lab: sent[k], file: other.request[k] });
167
+ }
168
+ rows.push({ id: p.id, status: fields.every(f => f.status === "same") ? "same" : "different", fields });
169
+ }
170
+ const ours = new Set(lab.map(p => p.id));
171
+ for (const p of app) if (!ours.has(p.id)) rows.push({ id: p.id, status: "file only", file: p.request });
172
+ return rows;
173
+ }
174
+
175
+ const shown = v => {
176
+ const s = JSON.stringify(v);
177
+ return s.length > 60 ? `${s.slice(0, 59)}…` : s;
178
+ };
179
+
180
+ // The comparison as text: a line per preset, then one per field not the same.
181
+ function table(rows) {
182
+ if (!rows.length) return "No presets on either side.";
183
+ const width = Math.max(...rows.map(r => r.id.length));
184
+ const lines = [];
185
+ for (const r of rows) {
186
+ lines.push(`${r.id.padEnd(width)} ${r.status}`);
187
+ const fields = r.fields ? r.fields.filter(f => f.status !== "same")
188
+ : Object.entries(r.lab ?? r.file).map(([field, v]) => ({ field, status: r.status, [r.lab ? "lab" : "file"]: v }));
189
+ for (const f of fields) {
190
+ const said = f.status === "different" ? `lab ${shown(f.lab)}, file ${shown(f.file)}`
191
+ : f.status === "lab only" ? `lab ${shown(f.lab)}` : `file ${shown(f.file)}`;
192
+ lines.push(`${"".padEnd(width)} ${f.field}: ${said}`);
193
+ }
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+
198
+ /**
199
+ * Runs [argv] -- what follows `evals-lab presets` -- against the staged core
200
+ * in [lab], and resolves to the exit code.
201
+ */
202
+ async function presets(argv, { lab, env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
203
+ const say = text => stderr.write(text + "\n");
204
+ try {
205
+ const o = options(argv);
206
+ if (o.help) { stdout.write(USAGE + "\n"); return 0; }
207
+ const corePath = path.join(lab, "evals-core.mjs");
208
+ if (!fs.existsSync(corePath)) {
209
+ throw new Refused(`no ${corePath} -- in a checkout the package is staged by \`node npm/build.js\``);
210
+ }
211
+ const core = require(corePath);
212
+ let text;
213
+ try { text = fs.readFileSync(path.resolve(o.file), "utf8"); }
214
+ catch (e) { throw new Refused(`${o.file}: ${e.message}`); }
215
+ const app = appPresets(o.file, text);
216
+ const ours = o.pack !== null ? packPresets(o.pack)
217
+ : await labApi(o.lab.replace(/\/+$/, ""), env, o.workspace)("GET", "/api/presets");
218
+ const rows = compare(core, ours, app);
219
+ const out = table(rows);
220
+ if (o.json === "-") { stdout.write(JSON.stringify({ presets: rows }, null, 2) + "\n"); say(out); }
221
+ else {
222
+ if (o.json) fs.writeFileSync(path.resolve(o.json), JSON.stringify({ presets: rows }, null, 2) + "\n");
223
+ stdout.write(out + "\n");
224
+ }
225
+ return o.failOnDifference && rows.some(r => r.status !== "same") ? EXIT.different : EXIT.same;
226
+ } catch (e) {
227
+ if (!(e instanceof Refused)) throw e;
228
+ say(`evals-lab presets: ${e.message}`);
229
+ if (e.usage) say(`\n${USAGE}`);
230
+ return EXIT.broken;
231
+ }
232
+ }
233
+
234
+ module.exports = { presets, options, compare, presetRequest, packPresets, appPresets, unzip, table, USAGE, FORMAT };
package/lab/VERSION CHANGED
@@ -1 +1 @@
1
- 0.14.0 (2026.10.14-830)
1
+ 0.16.0 (2026.10.16-944)
@@ -3,9 +3,9 @@
3
3
  "version": 1,
4
4
  "id": "demo",
5
5
  "name": "Demo",
6
- "packVersion": "2026.10.14.2",
6
+ "packVersion": "2026.10.14.3",
7
7
  "requires": {
8
- "lab": ">=18",
8
+ "lab": ">=19",
9
9
  "plugins": []
10
10
  },
11
11
  "description": "Six demos, each a dataset with a starter pipeline: two sets of public-domain images, short texts to classify, one question in four wordings, a dynamic Source's snapshot, and PDFs to check and sort."