@axlsdk/eval 0.17.9 → 0.18.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/dist/cli.js CHANGED
@@ -2,9 +2,11 @@
2
2
  import {
3
3
  aggregateRuns,
4
4
  evalCompare,
5
+ evaluateScorerErrorRateGate,
5
6
  rescore,
6
- runEval
7
- } from "./chunk-NETD4AUB.js";
7
+ runEval,
8
+ scorerCounts
9
+ } from "./chunk-BWBX5YVQ.js";
8
10
 
9
11
  // src/cli.ts
10
12
  import { readdirSync, statSync } from "fs";
@@ -57,10 +59,22 @@ function validateEvalConfig(cfg) {
57
59
  if (badScorerIdx !== -1) {
58
60
  return `exports a scorers array with a non-scorer entry at index ${badScorerIdx} \u2014 each scorer must have a score() method (use the scorer() factory).`;
59
61
  }
62
+ for (const key of ["concurrency", "scorerConcurrency"]) {
63
+ const v = c[key];
64
+ if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
65
+ return `exports an invalid ${key} (${typeof v === "number" ? v : typeof v}) \u2014 must be a positive integer.`;
66
+ }
67
+ }
68
+ {
69
+ const v = c.failOnScorerErrorRate;
70
+ if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)) {
71
+ return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
72
+ }
73
+ }
60
74
  return void 0;
61
75
  }
62
76
 
63
- // src/cli.ts
77
+ // src/cli-args.ts
64
78
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
65
79
  "--output",
66
80
  "--config",
@@ -68,8 +82,97 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
68
82
  "--fail-on-regression",
69
83
  "--threshold",
70
84
  "--runs",
71
- "--capture-traces"
85
+ "--capture-traces",
86
+ "--concurrency",
87
+ "--scorers"
72
88
  ]);
89
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
90
+ "--output",
91
+ "--config",
92
+ "--conditions",
93
+ "--runs",
94
+ "--concurrency",
95
+ "--scorers"
96
+ ]);
97
+ function envInt(name) {
98
+ const raw = process.env[name];
99
+ if (raw == null || raw === "") return void 0;
100
+ const n = parseInt(raw, 10);
101
+ if (!Number.isFinite(n) || n <= 0) return void 0;
102
+ return n;
103
+ }
104
+ function parseEvalArgs(args) {
105
+ let outputPath;
106
+ let configArg;
107
+ let conditions = [];
108
+ let runs = 1;
109
+ let captureTraces = false;
110
+ let concurrency;
111
+ let scorerNames;
112
+ const paths = [];
113
+ for (let i = 0; i < args.length; i++) {
114
+ const arg = args[i];
115
+ if (VALUE_FLAGS.has(arg)) {
116
+ if (i + 1 >= args.length) {
117
+ console.error(`Error: ${arg} requires a value`);
118
+ process.exit(1);
119
+ }
120
+ const value = args[++i];
121
+ if (arg === "--output") outputPath = value;
122
+ else if (arg === "--config") configArg = value;
123
+ else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
124
+ else if (arg === "--concurrency") {
125
+ const n = parseInt(value, 10);
126
+ if (!Number.isFinite(n) || n <= 0) {
127
+ console.error(`[axl-eval] Ignoring invalid --concurrency "${value}"; using 1.`);
128
+ concurrency = 1;
129
+ } else {
130
+ concurrency = n;
131
+ }
132
+ } else if (arg === "--scorers") {
133
+ scorerNames = [
134
+ ...new Set(
135
+ value.split(",").map((s) => s.trim()).filter(Boolean)
136
+ )
137
+ ];
138
+ } else {
139
+ conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
140
+ }
141
+ } else if (arg === "--capture-traces") {
142
+ captureTraces = true;
143
+ } else if (arg.startsWith("--")) {
144
+ if (!KNOWN_FLAGS.has(arg)) {
145
+ console.error(`Unknown flag: ${arg}`);
146
+ process.exit(1);
147
+ }
148
+ } else {
149
+ paths.push(arg);
150
+ }
151
+ }
152
+ return {
153
+ outputPath,
154
+ configArg,
155
+ conditions,
156
+ runs,
157
+ captureTraces,
158
+ concurrency,
159
+ scorerNames,
160
+ paths
161
+ };
162
+ }
163
+
164
+ // src/cli.ts
165
+ function refuseToGate(reason) {
166
+ console.error(`[axl-eval] Refusing to gate: ${reason}`);
167
+ process.exit(1);
168
+ }
169
+ function scorerSampleCounts(entry, name, items) {
170
+ if (entry.scored != null && entry.failed != null) {
171
+ return { scored: entry.scored, failed: entry.failed };
172
+ }
173
+ if (items) return scorerCounts(items, name);
174
+ return { scored: entry.scored ?? 0, failed: entry.failed ?? 0 };
175
+ }
73
176
  function installAbortHandler(controller) {
74
177
  let signalled = false;
75
178
  const onSignal = (sig) => {
@@ -96,6 +199,15 @@ Usage:
96
199
  axl-eval <path> --output <file> Save results to JSON
97
200
  axl-eval <path> --config <file> Use config file for runtime
98
201
  axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
202
+ axl-eval <path> --concurrency <n> Override item concurrency (flag > env
203
+ AXL_EVAL_CONCURRENCY > config > 5).
204
+ Max concurrent scorer calls is
205
+ concurrency x scorerConcurrency (the
206
+ per-eval scorerConcurrency defaults to 5).
207
+ axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
208
+ file only). Stamps the result as a
209
+ subset so compare won't treat it as a
210
+ full baseline.
99
211
  axl-eval <path> --capture-traces Populate EvalItem.traces on every item
100
212
  (success + failure). Adds memory
101
213
  overhead proportional to dataset size
@@ -104,6 +216,12 @@ Usage:
104
216
  axl-eval compare <a> <b> Compare two eval result files
105
217
  axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
106
218
  axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
219
+ axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
220
+ failure rate exceeds the limit on
221
+ either side (deterministic scorers
222
+ tolerate zero failures). Always warns
223
+ when either side rests on a thinned
224
+ sample, even without this flag.
107
225
 
108
226
  Config auto-detection (when --config is not specified):
109
227
  ${CONFIG_CANDIDATES.join(" -> ")}
@@ -138,20 +256,61 @@ function parseThresholdArg(args) {
138
256
  }
139
257
  return Object.keys(map).length > 0 ? map : void 0;
140
258
  }
259
+ function parseMaxScorerErrorRate(args) {
260
+ const idx = args.indexOf("--max-scorer-error-rate");
261
+ if (idx === -1) return void 0;
262
+ if (idx + 1 >= args.length) {
263
+ console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
264
+ process.exit(1);
265
+ }
266
+ const raw = args[idx + 1];
267
+ const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
268
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
269
+ console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
270
+ process.exit(1);
271
+ }
272
+ return n;
273
+ }
274
+ function scorerFilteredScorers(r) {
275
+ const first = Array.isArray(r) ? r[0] : r;
276
+ const meta = first?.metadata;
277
+ if (!meta || meta.scorerFiltered !== true) return void 0;
278
+ return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
279
+ }
141
280
  async function runCompare(args) {
142
281
  const failOnRegression = args.includes("--fail-on-regression");
143
282
  const thresholds = parseThresholdArg(args);
283
+ const maxScorerErrorRate = parseMaxScorerErrorRate(args);
284
+ const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
144
285
  const files = args.filter(
145
- (a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1] === "--threshold")
286
+ (a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
146
287
  );
147
288
  if (files.length !== 2) {
148
- console.error("Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>]");
289
+ console.error(
290
+ "Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
291
+ );
149
292
  process.exit(1);
150
293
  }
151
294
  const baselineRaw = JSON.parse(await readFileAsync(files[0], "utf-8"));
152
295
  const candidateRaw = JSON.parse(await readFileAsync(files[1], "utf-8"));
153
296
  const baseline = baselineRaw;
154
297
  const candidate = candidateRaw;
298
+ const filteredSides = [
299
+ ["baseline", baseline],
300
+ ["candidate", candidate]
301
+ ].filter(([, r]) => scorerFilteredScorers(r) != null);
302
+ for (const [side, r] of filteredSides) {
303
+ const ran = scorerFilteredScorers(r);
304
+ const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
305
+ console.error(
306
+ `[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
307
+ );
308
+ }
309
+ if (failOnRegression && filteredSides.length > 0) {
310
+ refuseToGate(
311
+ `${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
312
+ );
313
+ }
155
314
  const compareOptions = thresholds != null ? { thresholds } : void 0;
156
315
  const comparison = evalCompare(baseline, candidate, compareOptions);
157
316
  console.log(
@@ -207,6 +366,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
207
366
  Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
208
367
  `
209
368
  );
369
+ let sawThinnedSample = false;
370
+ for (const name of scorerNames) {
371
+ const s = comparison.scorers[name];
372
+ const bFailed = s.baselineFailed ?? 0;
373
+ const cFailed = s.candidateFailed ?? 0;
374
+ if (bFailed > 0 || cFailed > 0) {
375
+ sawThinnedSample = true;
376
+ const bAtt = (s.baselineScored ?? 0) + bFailed;
377
+ const cAtt = (s.candidateScored ?? 0) + cFailed;
378
+ console.error(
379
+ `[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
380
+ );
381
+ }
382
+ }
383
+ if (sawThinnedSample && maxScorerErrorRate == null) {
384
+ console.error(
385
+ `[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
386
+ );
387
+ }
388
+ if (maxScorerErrorRate != null) {
389
+ const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
390
+ if (reason) refuseToGate(reason);
391
+ }
210
392
  if (failOnRegression && comparison.regressions.length > 0) {
211
393
  const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
212
394
  if (hasSignificance) {
@@ -220,11 +402,18 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
220
402
  }
221
403
  }
222
404
  async function runRescore(args, signal) {
223
- const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
405
+ const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
224
406
  if (paths.length < 2) {
225
407
  console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
226
408
  process.exit(1);
227
409
  }
410
+ if (scorerNames?.length) {
411
+ console.error(
412
+ "Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
413
+ );
414
+ process.exit(1);
415
+ }
416
+ const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
228
417
  const [resultsPath, evalFilePath] = paths;
229
418
  const raw = JSON.parse(await readFileAsync(resultsPath, "utf-8"));
230
419
  const results = Array.isArray(raw) ? raw : [raw];
@@ -245,7 +434,12 @@ async function runRescore(args, signal) {
245
434
  const rescored = [];
246
435
  for (const resultData of results) {
247
436
  if (signal.aborted) break;
248
- rescored.push(await rescore(resultData, evalConfig.scorers, runtime, { signal }));
437
+ rescored.push(
438
+ await rescore(resultData, evalConfig.scorers, runtime, {
439
+ signal,
440
+ ...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
441
+ })
442
+ );
249
443
  }
250
444
  for (const r of rescored) {
251
445
  console.log("\n" + formatTable(r) + "\n");
@@ -295,14 +489,15 @@ function formatTable(result) {
295
489
  lines.push(" " + "\u2500".repeat(ruleLen));
296
490
  for (const name of scorerNames) {
297
491
  const s = result.summary.scorers[name];
298
- const validScoreCount = result.items.filter((i) => !i.error && i.scores[name] != null).length;
299
- if (validScoreCount === 0) {
492
+ const { scored, failed } = scorerSampleCounts(s, name, result.items);
493
+ const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
494
+ if (scored === 0) {
300
495
  lines.push(
301
- ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
496
+ ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
302
497
  );
303
498
  } else {
304
499
  lines.push(
305
- ` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}`
500
+ ` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}${annotation}`
306
501
  );
307
502
  }
308
503
  }
@@ -350,8 +545,10 @@ function formatMultiRunTable(summary) {
350
545
  for (const name of scorerNames) {
351
546
  const s = summary.scorers[name];
352
547
  const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
548
+ const { scored, failed } = scorerSampleCounts(s, name);
549
+ const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
353
550
  lines.push(
354
- ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
551
+ ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
355
552
  );
356
553
  }
357
554
  if (summary.timing) {
@@ -365,6 +562,26 @@ function formatMultiRunTable(summary) {
365
562
  lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
366
563
  return lines.join("\n");
367
564
  }
565
+ function reportDegraded(result, label) {
566
+ const d = result.summary.degraded;
567
+ if (!d || d.length === 0) return false;
568
+ for (const x of d) {
569
+ const attempted = x.scored + x.failed;
570
+ const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
571
+ console.error(
572
+ `[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
573
+ );
574
+ }
575
+ return true;
576
+ }
577
+ function reportTotalWipeout(result, label) {
578
+ const { count, failures } = result.summary;
579
+ if (count === 0 || failures < count) return false;
580
+ console.error(
581
+ `[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
582
+ );
583
+ return true;
584
+ }
368
585
  async function resolveRuntimeFromConfig(configPath) {
369
586
  try {
370
587
  const mod = await importModule(configPath, import.meta.url);
@@ -425,41 +642,17 @@ async function getRuntime(configArg, conditions) {
425
642
  const { AxlRuntime } = await import("@axlsdk/axl");
426
643
  return new AxlRuntime();
427
644
  }
428
- function parseEvalArgs(args) {
429
- let outputPath;
430
- let configArg;
431
- let conditions = [];
432
- let runs = 1;
433
- let captureTraces = false;
434
- const paths = [];
435
- for (let i = 0; i < args.length; i++) {
436
- const arg = args[i];
437
- if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
438
- if (i + 1 >= args.length) {
439
- console.error(`Error: ${arg} requires a value`);
440
- process.exit(1);
441
- }
442
- const value = args[++i];
443
- if (arg === "--output") outputPath = value;
444
- else if (arg === "--config") configArg = value;
445
- else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
446
- else
447
- conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
448
- } else if (arg === "--capture-traces") {
449
- captureTraces = true;
450
- } else if (arg.startsWith("--")) {
451
- if (!KNOWN_FLAGS.has(arg)) {
452
- console.error(`Unknown flag: ${arg}`);
453
- process.exit(1);
454
- }
455
- } else {
456
- paths.push(arg);
457
- }
458
- }
459
- return { outputPath, configArg, conditions, runs, captureTraces, paths };
460
- }
461
645
  async function runEvalCommand(args, signal) {
462
- const { outputPath, configArg, conditions, runs, captureTraces, paths } = parseEvalArgs(args);
646
+ const {
647
+ outputPath,
648
+ configArg,
649
+ conditions,
650
+ runs,
651
+ captureTraces,
652
+ concurrency,
653
+ scorerNames,
654
+ paths
655
+ } = parseEvalArgs(args);
463
656
  if (paths.length === 0) {
464
657
  console.error("Error: No eval file path provided");
465
658
  process.exit(1);
@@ -472,6 +665,10 @@ async function runEvalCommand(args, signal) {
472
665
  console.error("Error: No eval files found");
473
666
  process.exit(1);
474
667
  }
668
+ if (scorerNames?.length && evalFiles.length > 1) {
669
+ console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
670
+ process.exit(1);
671
+ }
475
672
  const runtime = await getRuntime(configArg, conditions);
476
673
  const results = [];
477
674
  let failedFiles = 0;
@@ -487,6 +684,22 @@ async function runEvalCommand(args, signal) {
487
684
  failedFiles++;
488
685
  continue;
489
686
  }
687
+ evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
688
+ if (scorerNames?.length) {
689
+ const available = evalConfig.scorers.map((s) => s.name);
690
+ const unknown = scorerNames.filter((n) => !available.includes(n));
691
+ if (unknown.length) {
692
+ console.error(
693
+ `Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
694
+ );
695
+ process.exit(1);
696
+ }
697
+ evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
698
+ if (evalConfig.scorers.length === 0) {
699
+ console.error("Error: --scorers matched no scorers.");
700
+ process.exit(1);
701
+ }
702
+ }
490
703
  const exported = pickExport(mod, "executeWorkflow");
491
704
  let customExecute;
492
705
  if (typeof exported === "function") {
@@ -543,6 +756,13 @@ async function runEvalCommand(args, signal) {
543
756
  continue;
544
757
  }
545
758
  const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
759
+ const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
760
+ const stampFiltered = (result) => {
761
+ if (scorersRun) {
762
+ result.metadata.scorerFiltered = true;
763
+ result.metadata.scorersRun = scorersRun;
764
+ }
765
+ };
546
766
  if (runs > 1) {
547
767
  const { randomUUID } = await import("crypto");
548
768
  const runGroupId = randomUUID();
@@ -555,6 +775,7 @@ async function runEvalCommand(args, signal) {
555
775
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
556
776
  result.metadata.runGroupId = runGroupId;
557
777
  result.metadata.runIndex = r;
778
+ stampFiltered(result);
558
779
  runResults.push(result);
559
780
  } catch (err) {
560
781
  runFailure = err instanceof Error ? err : new Error(String(err));
@@ -586,10 +807,21 @@ async function runEvalCommand(args, signal) {
586
807
  const summary = aggregateRuns(runResults);
587
808
  console.log("\n" + formatMultiRunTable(summary) + "\n");
588
809
  for (const r of runResults) results.push(r);
810
+ let anyFailing = false;
811
+ for (const r of runResults) {
812
+ const wipeout = reportTotalWipeout(r, filePath);
813
+ const degraded = reportDegraded(r, filePath);
814
+ if (wipeout || degraded) anyFailing = true;
815
+ }
816
+ if (anyFailing && !partial) failedFiles++;
589
817
  } else {
590
818
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
819
+ stampFiltered(result);
591
820
  results.push(result);
592
821
  console.log("\n" + formatTable(result) + "\n");
822
+ const wipeout = reportTotalWipeout(result, filePath);
823
+ const degraded = reportDegraded(result, filePath);
824
+ if (wipeout || degraded) failedFiles++;
593
825
  }
594
826
  } catch (err) {
595
827
  console.error(