@axlsdk/eval 0.17.8 → 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.cjs CHANGED
@@ -94,10 +94,52 @@ function xorshift32(seed) {
94
94
  };
95
95
  }
96
96
 
97
- // src/compare.ts
97
+ // src/utils.ts
98
+ function computeStats(scores) {
99
+ if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
100
+ const sorted = [...scores].sort((a, b) => a - b);
101
+ const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
102
+ const min = sorted[0];
103
+ const max = sorted[sorted.length - 1];
104
+ const p50 = sorted[Math.floor(sorted.length * 0.5)];
105
+ const p95 = sorted[Math.floor(sorted.length * 0.95)];
106
+ return { mean: round2(mean), min: round2(min), max: round2(max), p50: round2(p50), p95: round2(p95) };
107
+ }
98
108
  function round2(n) {
99
109
  return Math.round(n * 1e3) / 1e3;
100
110
  }
111
+ function scorerCounts(items, name) {
112
+ let scored = 0;
113
+ let failed = 0;
114
+ for (const i of items) {
115
+ if (i.error) continue;
116
+ if (i.scores[name] != null) scored++;
117
+ else if (i.scoreDetails?.[name]?.duration != null) failed++;
118
+ }
119
+ return { scored, failed };
120
+ }
121
+ function evaluateScorerTolerance(scored, failed, type, limit) {
122
+ const attempted = scored + failed;
123
+ const rate = attempted === 0 ? 0 : failed / attempted;
124
+ const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
125
+ return { attempted, rate, exceeds, zeroSample: attempted === 0 };
126
+ }
127
+ async function mapWithConcurrency(items, concurrency, task) {
128
+ const results = new Array(items.length);
129
+ const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
130
+ const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
131
+ let next = 0;
132
+ async function runNext() {
133
+ while (next < items.length) {
134
+ const i = next++;
135
+ results[i] = await task(items[i], i);
136
+ }
137
+ }
138
+ await Promise.all(Array.from({ length: workerCount }, () => runNext()));
139
+ return results;
140
+ }
141
+
142
+ // src/compare.ts
101
143
  var DEFAULT_LLM_THRESHOLD = 0.05;
102
144
  var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
103
145
  var LEGACY_THRESHOLD = 0.1;
@@ -171,11 +213,29 @@ function evalCompare(baseline, candidate, options) {
171
213
  const delta = round2(deltaRaw);
172
214
  const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
173
215
  const threshold = resolveThreshold(name, options, baselineRef.metadata);
216
+ let baselineScored = 0;
217
+ let baselineFailed = 0;
218
+ for (const run of baselineRuns) {
219
+ const c = scorerCounts(run.items, name);
220
+ baselineScored += c.scored;
221
+ baselineFailed += c.failed;
222
+ }
223
+ let candidateScored = 0;
224
+ let candidateFailed = 0;
225
+ for (const run of candidateRuns) {
226
+ const c = scorerCounts(run.items, name);
227
+ candidateScored += c.scored;
228
+ candidateFailed += c.failed;
229
+ }
174
230
  const entry = {
175
231
  baselineMean: bMean,
176
232
  candidateMean: cMean,
177
233
  delta,
178
- deltaPercent
234
+ deltaPercent,
235
+ baselineScored,
236
+ baselineFailed,
237
+ candidateScored,
238
+ candidateFailed
179
239
  };
180
240
  const diffs = pairedDiffs[name];
181
241
  if (diffs.length >= 2) {
@@ -311,25 +371,91 @@ function detectPartial(runs) {
311
371
  if (runs.length >= attempted) return void 0;
312
372
  return { completed: runs.length, attempted };
313
373
  }
374
+ function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
375
+ const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
376
+ for (const name of Object.keys(comparison.scorers)) {
377
+ const s = comparison.scorers[name];
378
+ const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
379
+ const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
380
+ for (const side of ["baseline", "candidate"]) {
381
+ const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
382
+ const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
383
+ const verdict = evaluateScorerTolerance(scored, failed, type, limit);
384
+ if (verdict.zeroSample) {
385
+ return `scorer "${name}" has no scored items on ${side} (its items errored or were skipped) \u2014 cannot certify a mean over an empty sample.`;
386
+ }
387
+ if (verdict.exceeds) {
388
+ const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
389
+ return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
390
+ }
391
+ }
392
+ }
393
+ return null;
394
+ }
314
395
 
315
396
  // src/scorer.ts
316
397
  function normalizeScorerResult(result) {
317
398
  return typeof result === "number" ? { score: result } : result;
318
399
  }
319
-
320
- // src/utils.ts
321
- function computeStats(scores) {
322
- if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };
323
- const sorted = [...scores].sort((a, b) => a - b);
324
- const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;
325
- const min = sorted[0];
326
- const max = sorted[sorted.length - 1];
327
- const p50 = sorted[Math.floor(sorted.length * 0.5)];
328
- const p95 = sorted[Math.floor(sorted.length * 0.95)];
329
- return { mean: round3(mean), min: round3(min), max: round3(max), p50: round3(p50), p95: round3(p95) };
400
+ function extractScorerErrorCost(err) {
401
+ const v = err && typeof err === "object" ? err.cost : void 0;
402
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
330
403
  }
331
- function round3(n) {
332
- return Math.round(n * 1e3) / 1e3;
404
+
405
+ // src/score-item.ts
406
+ async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
407
+ const scoreDetails = item.scoreDetails ??= {};
408
+ for (const s of scorers) {
409
+ item.scores[s.name] = null;
410
+ scoreDetails[s.name] = { score: null };
411
+ }
412
+ let itemScorerCost = 0;
413
+ const scorerErrorsByName = {};
414
+ await mapWithConcurrency(scorers, scorerConcurrency, async (scorer) => {
415
+ if (signal?.aborted) return;
416
+ const scorerStart = Date.now();
417
+ try {
418
+ const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
419
+ const scorerResult = normalizeScorerResult(raw);
420
+ if (scorerResult.cost != null) {
421
+ itemScorerCost += scorerResult.cost;
422
+ }
423
+ const scorerDuration = Date.now() - scorerStart;
424
+ if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
425
+ scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
426
+ item.scores[scorer.name] = null;
427
+ scoreDetails[scorer.name] = {
428
+ score: null,
429
+ metadata: scorerResult.metadata,
430
+ duration: scorerDuration,
431
+ cost: scorerResult.cost
432
+ };
433
+ } else {
434
+ item.scores[scorer.name] = round2(scorerResult.score);
435
+ scoreDetails[scorer.name] = {
436
+ score: round2(scorerResult.score),
437
+ metadata: scorerResult.metadata,
438
+ duration: scorerDuration,
439
+ cost: scorerResult.cost
440
+ };
441
+ }
442
+ } catch (err) {
443
+ if (err?.name === "AbortError") return;
444
+ const errCost = extractScorerErrorCost(err);
445
+ if (errCost != null) itemScorerCost += errCost;
446
+ scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
447
+ item.scores[scorer.name] = null;
448
+ scoreDetails[scorer.name] = {
449
+ score: null,
450
+ duration: Date.now() - scorerStart,
451
+ cost: errCost
452
+ };
453
+ }
454
+ });
455
+ const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
456
+ if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
457
+ item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
458
+ return itemScorerCost;
333
459
  }
334
460
 
335
461
  // src/runner.ts
@@ -372,7 +498,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
372
498
  const startTime = Date.now();
373
499
  const id = (0, import_node_crypto.randomUUID)();
374
500
  const items = await config.dataset.getItems();
501
+ const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
375
502
  const concurrency = config.concurrency ?? 5;
503
+ const scorerConcurrency = config.scorerConcurrency ?? 5;
376
504
  const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
377
505
  const scorerContext = {
378
506
  resolveProvider: (uri) => {
@@ -382,7 +510,8 @@ async function runEval(config, executeWorkflow, runtime, options) {
382
510
  );
383
511
  }
384
512
  return runtime.resolveProvider(uri);
385
- }
513
+ },
514
+ signal: options?.signal
386
515
  };
387
516
  const evalItems = new Array(items.length);
388
517
  let totalCost = 0;
@@ -460,89 +589,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
460
589
  if (budgetLimit != null && totalCost > budgetLimit) {
461
590
  budgetExceeded = true;
462
591
  }
463
- evalItem.scoreDetails = {};
464
- let itemScorerCost = 0;
465
- for (const scorer of config.scorers) {
466
- if (options?.signal?.aborted) break;
467
- const scorerStart = Date.now();
468
- try {
469
- const raw = await scorer.score(
470
- evalItem.output,
471
- item.input,
472
- item.annotations,
473
- scorerContext
474
- );
475
- const scorerResult = normalizeScorerResult(raw);
476
- if (scorerResult.cost != null) {
477
- itemScorerCost += scorerResult.cost;
478
- totalCost += scorerResult.cost;
479
- }
480
- const scorerDuration = Date.now() - scorerStart;
481
- if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
482
- if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
483
- evalItem.scorerErrors.push(
484
- `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
485
- );
486
- evalItem.scores[scorer.name] = null;
487
- evalItem.scoreDetails[scorer.name] = {
488
- score: null,
489
- metadata: scorerResult.metadata,
490
- duration: scorerDuration,
491
- cost: scorerResult.cost
492
- };
493
- } else {
494
- evalItem.scores[scorer.name] = round3(scorerResult.score);
495
- evalItem.scoreDetails[scorer.name] = {
496
- score: round3(scorerResult.score),
497
- metadata: scorerResult.metadata,
498
- duration: scorerDuration,
499
- cost: scorerResult.cost
500
- };
501
- }
502
- } catch (err) {
503
- const errCost = typeof err?.cost === "number" ? err.cost : void 0;
504
- if (errCost != null) {
505
- itemScorerCost += errCost;
506
- totalCost += errCost;
507
- }
508
- if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
509
- evalItem.scorerErrors.push(
510
- `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
511
- );
512
- evalItem.scores[scorer.name] = null;
513
- evalItem.scoreDetails[scorer.name] = {
514
- score: null,
515
- duration: Date.now() - scorerStart,
516
- cost: errCost
517
- };
518
- }
519
- }
520
- evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
592
+ const itemScorerCost = await scoreItem(
593
+ evalItem,
594
+ config.scorers,
595
+ scorerConcurrency,
596
+ scorerContext,
597
+ options?.signal
598
+ );
599
+ totalCost += itemScorerCost;
521
600
  evalItems[itemIndex] = evalItem;
522
601
  options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
523
602
  }
524
- let index = 0;
525
- async function runNext() {
526
- while (index < items.length) {
527
- const currentIndex = index++;
528
- await processItem(items[currentIndex], currentIndex);
529
- }
530
- }
531
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
532
- await Promise.all(workers);
603
+ await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
533
604
  const failures = evalItems.filter((i) => i.error).length;
534
605
  const scorerNames = config.scorers.map((s) => s.name);
535
606
  const scorerStats = {};
536
607
  for (const name of scorerNames) {
537
608
  const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
538
- scorerStats[name] = computeStats(scores);
609
+ const { scored, failed } = scorerCounts(evalItems, name);
610
+ scorerStats[name] = { ...computeStats(scores), scored, failed };
539
611
  }
540
- const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
541
- const timing = durations.length > 0 ? computeStats(durations) : void 0;
542
612
  const scorerTypes = {};
543
613
  for (const s of config.scorers) {
544
614
  scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
545
615
  }
616
+ let degraded;
617
+ const rawLimit = config.failOnScorerErrorRate;
618
+ if (rawLimit != null) {
619
+ if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
620
+ console.warn(
621
+ `[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
622
+ );
623
+ } else {
624
+ for (const name of scorerNames) {
625
+ const stats = scorerStats[name];
626
+ const scored = stats.scored ?? 0;
627
+ const failed = stats.failed ?? 0;
628
+ const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
629
+ const limit = type === "deterministic" ? 0 : rawLimit;
630
+ const verdict = evaluateScorerTolerance(scored, failed, type, limit);
631
+ if (verdict.exceeds) {
632
+ (degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
633
+ }
634
+ }
635
+ }
636
+ }
637
+ const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
638
+ const timing = durations.length > 0 ? computeStats(durations) : void 0;
546
639
  const totalModelCalls = /* @__PURE__ */ new Map();
547
640
  for (const item of evalItems) {
548
641
  const itemCounts = item.metadata?.modelCallCounts;
@@ -601,13 +694,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
601
694
  ...config.metadata,
602
695
  scorerTypes,
603
696
  ...modelsMeta,
604
- ...workflowsMeta
697
+ ...workflowsMeta,
698
+ ...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
605
699
  },
606
700
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
607
701
  totalCost,
608
702
  duration: Date.now() - startTime,
609
703
  items: evalItems,
610
- summary: { count: items.length, failures, scorers: scorerStats, timing }
704
+ summary: {
705
+ count: items.length,
706
+ failures,
707
+ scorers: scorerStats,
708
+ timing,
709
+ ...degraded ? { degraded } : {}
710
+ }
611
711
  };
612
712
  }
613
713
 
@@ -616,6 +716,7 @@ var import_node_crypto2 = require("crypto");
616
716
  async function rescore(result, scorers, runtime, options) {
617
717
  const startTime = Date.now();
618
718
  const concurrency = options?.concurrency ?? 5;
719
+ const scorerConcurrency = options?.scorerConcurrency ?? 5;
619
720
  const scorerContext = {
620
721
  resolveProvider: (uri) => {
621
722
  if (typeof runtime.resolveProvider !== "function") {
@@ -624,11 +725,22 @@ async function rescore(result, scorers, runtime, options) {
624
725
  );
625
726
  }
626
727
  return runtime.resolveProvider(uri);
627
- }
728
+ },
729
+ signal: options?.signal
628
730
  };
629
731
  const rescored = new Array(result.items.length);
630
732
  let totalCost = 0;
631
733
  async function rescoreItem(original, itemIndex) {
734
+ if (options?.signal?.aborted) {
735
+ rescored[itemIndex] = {
736
+ input: original.input,
737
+ annotations: original.annotations,
738
+ output: original.output,
739
+ error: "cancelled",
740
+ scores: {}
741
+ };
742
+ return;
743
+ }
632
744
  if (original.error) {
633
745
  rescored[itemIndex] = {
634
746
  input: original.input,
@@ -648,77 +760,24 @@ async function rescore(result, scorers, runtime, options) {
648
760
  scores: {},
649
761
  scoreDetails: {}
650
762
  };
651
- let itemScorerCost = 0;
652
- for (const scorer of scorers) {
653
- const scorerStart = Date.now();
654
- try {
655
- const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
656
- const scorerResult = normalizeScorerResult(raw);
657
- if (scorerResult.cost != null) {
658
- itemScorerCost += scorerResult.cost;
659
- totalCost += scorerResult.cost;
660
- }
661
- const scorerDuration = Date.now() - scorerStart;
662
- if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
663
- if (!item.scorerErrors) item.scorerErrors = [];
664
- item.scorerErrors.push(
665
- `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
666
- );
667
- item.scores[scorer.name] = null;
668
- item.scoreDetails[scorer.name] = {
669
- score: null,
670
- metadata: scorerResult.metadata,
671
- duration: scorerDuration,
672
- cost: scorerResult.cost
673
- };
674
- } else {
675
- item.scores[scorer.name] = round3(scorerResult.score);
676
- item.scoreDetails[scorer.name] = {
677
- score: round3(scorerResult.score),
678
- metadata: scorerResult.metadata,
679
- duration: scorerDuration,
680
- cost: scorerResult.cost
681
- };
682
- }
683
- } catch (err) {
684
- const errCost = typeof err?.cost === "number" ? err.cost : void 0;
685
- if (errCost != null) {
686
- itemScorerCost += errCost;
687
- totalCost += errCost;
688
- }
689
- if (!item.scorerErrors) item.scorerErrors = [];
690
- item.scorerErrors.push(
691
- `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
692
- );
693
- item.scores[scorer.name] = null;
694
- item.scoreDetails[scorer.name] = {
695
- score: null,
696
- duration: Date.now() - scorerStart,
697
- cost: errCost
698
- };
699
- }
700
- }
701
- item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
763
+ const itemScorerCost = await scoreItem(
764
+ item,
765
+ scorers,
766
+ scorerConcurrency,
767
+ scorerContext,
768
+ options?.signal
769
+ );
770
+ totalCost += itemScorerCost;
702
771
  rescored[itemIndex] = item;
703
772
  }
704
- let index = 0;
705
- async function runNext() {
706
- while (index < result.items.length) {
707
- const currentIndex = index++;
708
- await rescoreItem(result.items[currentIndex], currentIndex);
709
- }
710
- }
711
- const workers = Array.from(
712
- { length: Math.min(concurrency, result.items.length) },
713
- () => runNext()
714
- );
715
- await Promise.all(workers);
773
+ await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
716
774
  const failures = rescored.filter((i) => i.error).length;
717
775
  const scorerNames = scorers.map((s) => s.name);
718
776
  const scorerStats = {};
719
777
  for (const name of scorerNames) {
720
778
  const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
721
- scorerStats[name] = computeStats(scores);
779
+ const { scored, failed } = scorerCounts(rescored, name);
780
+ scorerStats[name] = { ...computeStats(scores), scored, failed };
722
781
  }
723
782
  const scorerTypes = {};
724
783
  for (const s of scorers) {
@@ -728,7 +787,11 @@ async function rescore(result, scorers, runtime, options) {
728
787
  id: (0, import_node_crypto2.randomUUID)(),
729
788
  dataset: result.dataset,
730
789
  metadata: (() => {
731
- const { runGroupId: _, runIndex: __, ...rest } = result.metadata;
790
+ const rest = { ...result.metadata };
791
+ delete rest.runGroupId;
792
+ delete rest.runIndex;
793
+ delete rest.scorerFiltered;
794
+ delete rest.scorersRun;
732
795
  const merged = {
733
796
  ...rest,
734
797
  rescored: true,
@@ -781,15 +844,24 @@ function aggregateRuns(runs) {
781
844
  const scorerNames = Object.keys(runs[0].summary.scorers);
782
845
  const scorers = {};
783
846
  for (const name of scorerNames) {
784
- const means = runs.map((r) => r.summary.scorers[name]?.mean ?? 0);
847
+ const contributing = runs.filter((r) => {
848
+ const sc = r.summary.scorers[name]?.scored;
849
+ return sc == null || sc > 0;
850
+ });
851
+ const meanSource = contributing.length > 0 ? contributing : runs;
852
+ const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
785
853
  const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
786
854
  const minMean = Math.min(...means);
787
855
  const maxMean = Math.max(...means);
856
+ const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
857
+ const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
788
858
  scorers[name] = {
789
- mean: round3(meanOfMeans),
790
- std: round3(std(means, meanOfMeans)),
791
- min: round3(minMean),
792
- max: round3(maxMean)
859
+ mean: round2(meanOfMeans),
860
+ std: round2(std(means, meanOfMeans)),
861
+ min: round2(minMean),
862
+ max: round2(maxMean),
863
+ scored,
864
+ failed
793
865
  };
794
866
  }
795
867
  let timing;
@@ -797,8 +869,8 @@ function aggregateRuns(runs) {
797
869
  if (timingMeans.length === runs.length && timingMeans.length > 0) {
798
870
  const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
799
871
  timing = {
800
- mean: round3(meanTiming),
801
- std: round3(std(timingMeans, meanTiming))
872
+ mean: round2(meanTiming),
873
+ std: round2(std(timingMeans, meanTiming))
802
874
  };
803
875
  }
804
876
  return {
@@ -806,8 +878,8 @@ function aggregateRuns(runs) {
806
878
  runCount: runs.length,
807
879
  workflows,
808
880
  dataset,
809
- totalCost: round3(totalCost),
810
- totalDuration: round3(totalDuration),
881
+ totalCost: round2(totalCost),
882
+ totalDuration: round2(totalDuration),
811
883
  scorers,
812
884
  timing
813
885
  };
@@ -849,11 +921,22 @@ function validateEvalConfig(cfg) {
849
921
  if (badScorerIdx !== -1) {
850
922
  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).`;
851
923
  }
924
+ for (const key of ["concurrency", "scorerConcurrency"]) {
925
+ const v = c[key];
926
+ if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
927
+ return `exports an invalid ${key} (${typeof v === "number" ? v : typeof v}) \u2014 must be a positive integer.`;
928
+ }
929
+ }
930
+ {
931
+ const v = c.failOnScorerErrorRate;
932
+ if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)) {
933
+ return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
934
+ }
935
+ }
852
936
  return void 0;
853
937
  }
854
938
 
855
- // src/cli.ts
856
- var import_meta = {};
939
+ // src/cli-args.ts
857
940
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
858
941
  "--output",
859
942
  "--config",
@@ -861,8 +944,114 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
861
944
  "--fail-on-regression",
862
945
  "--threshold",
863
946
  "--runs",
864
- "--capture-traces"
947
+ "--capture-traces",
948
+ "--concurrency",
949
+ "--scorers"
865
950
  ]);
951
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
952
+ "--output",
953
+ "--config",
954
+ "--conditions",
955
+ "--runs",
956
+ "--concurrency",
957
+ "--scorers"
958
+ ]);
959
+ function envInt(name) {
960
+ const raw = process.env[name];
961
+ if (raw == null || raw === "") return void 0;
962
+ const n = parseInt(raw, 10);
963
+ if (!Number.isFinite(n) || n <= 0) return void 0;
964
+ return n;
965
+ }
966
+ function parseEvalArgs(args) {
967
+ let outputPath;
968
+ let configArg;
969
+ let conditions = [];
970
+ let runs = 1;
971
+ let captureTraces = false;
972
+ let concurrency;
973
+ let scorerNames;
974
+ const paths = [];
975
+ for (let i = 0; i < args.length; i++) {
976
+ const arg = args[i];
977
+ if (VALUE_FLAGS.has(arg)) {
978
+ if (i + 1 >= args.length) {
979
+ console.error(`Error: ${arg} requires a value`);
980
+ process.exit(1);
981
+ }
982
+ const value = args[++i];
983
+ if (arg === "--output") outputPath = value;
984
+ else if (arg === "--config") configArg = value;
985
+ else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
986
+ else if (arg === "--concurrency") {
987
+ const n = parseInt(value, 10);
988
+ if (!Number.isFinite(n) || n <= 0) {
989
+ console.error(`[axl-eval] Ignoring invalid --concurrency "${value}"; using 1.`);
990
+ concurrency = 1;
991
+ } else {
992
+ concurrency = n;
993
+ }
994
+ } else if (arg === "--scorers") {
995
+ scorerNames = [
996
+ ...new Set(
997
+ value.split(",").map((s) => s.trim()).filter(Boolean)
998
+ )
999
+ ];
1000
+ } else {
1001
+ conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
1002
+ }
1003
+ } else if (arg === "--capture-traces") {
1004
+ captureTraces = true;
1005
+ } else if (arg.startsWith("--")) {
1006
+ if (!KNOWN_FLAGS.has(arg)) {
1007
+ console.error(`Unknown flag: ${arg}`);
1008
+ process.exit(1);
1009
+ }
1010
+ } else {
1011
+ paths.push(arg);
1012
+ }
1013
+ }
1014
+ return {
1015
+ outputPath,
1016
+ configArg,
1017
+ conditions,
1018
+ runs,
1019
+ captureTraces,
1020
+ concurrency,
1021
+ scorerNames,
1022
+ paths
1023
+ };
1024
+ }
1025
+
1026
+ // src/cli.ts
1027
+ var import_meta = {};
1028
+ function refuseToGate(reason) {
1029
+ console.error(`[axl-eval] Refusing to gate: ${reason}`);
1030
+ process.exit(1);
1031
+ }
1032
+ function scorerSampleCounts(entry, name, items) {
1033
+ if (entry.scored != null && entry.failed != null) {
1034
+ return { scored: entry.scored, failed: entry.failed };
1035
+ }
1036
+ if (items) return scorerCounts(items, name);
1037
+ return { scored: entry.scored ?? 0, failed: entry.failed ?? 0 };
1038
+ }
1039
+ function installAbortHandler(controller) {
1040
+ let signalled = false;
1041
+ const onSignal = (sig) => {
1042
+ if (signalled) {
1043
+ console.error(`
1044
+ [axl-eval] Second ${sig} \u2014 forcing exit.`);
1045
+ process.exit(130);
1046
+ }
1047
+ signalled = true;
1048
+ console.error(`
1049
+ [axl-eval] ${sig} received \u2014 cancelling eval (press again to force exit)...`);
1050
+ controller.abort();
1051
+ };
1052
+ process.on("SIGINT", onSignal);
1053
+ process.on("SIGTERM", onSignal);
1054
+ }
866
1055
  async function main() {
867
1056
  const args = process.argv.slice(2);
868
1057
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
@@ -873,6 +1062,15 @@ Usage:
873
1062
  axl-eval <path> --output <file> Save results to JSON
874
1063
  axl-eval <path> --config <file> Use config file for runtime
875
1064
  axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
1065
+ axl-eval <path> --concurrency <n> Override item concurrency (flag > env
1066
+ AXL_EVAL_CONCURRENCY > config > 5).
1067
+ Max concurrent scorer calls is
1068
+ concurrency x scorerConcurrency (the
1069
+ per-eval scorerConcurrency defaults to 5).
1070
+ axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
1071
+ file only). Stamps the result as a
1072
+ subset so compare won't treat it as a
1073
+ full baseline.
876
1074
  axl-eval <path> --capture-traces Populate EvalItem.traces on every item
877
1075
  (success + failure). Adds memory
878
1076
  overhead proportional to dataset size
@@ -881,6 +1079,12 @@ Usage:
881
1079
  axl-eval compare <a> <b> Compare two eval result files
882
1080
  axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
883
1081
  axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
1082
+ axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
1083
+ failure rate exceeds the limit on
1084
+ either side (deterministic scorers
1085
+ tolerate zero failures). Always warns
1086
+ when either side rests on a thinned
1087
+ sample, even without this flag.
884
1088
 
885
1089
  Config auto-detection (when --config is not specified):
886
1090
  ${import_axl.CONFIG_CANDIDATES.join(" -> ")}
@@ -895,11 +1099,13 @@ When no config is found, a bare AxlRuntime is created (providers from env vars).
895
1099
  await runCompare(args.slice(1));
896
1100
  return;
897
1101
  }
1102
+ const controller = new AbortController();
1103
+ installAbortHandler(controller);
898
1104
  if (args[0] === "rescore") {
899
- await runRescore(args.slice(1));
1105
+ await runRescore(args.slice(1), controller.signal);
900
1106
  return;
901
1107
  }
902
- await runEvalCommand(args);
1108
+ await runEvalCommand(args, controller.signal);
903
1109
  }
904
1110
  function parseThresholdArg(args) {
905
1111
  const idx = args.indexOf("--threshold");
@@ -913,20 +1119,61 @@ function parseThresholdArg(args) {
913
1119
  }
914
1120
  return Object.keys(map).length > 0 ? map : void 0;
915
1121
  }
1122
+ function parseMaxScorerErrorRate(args) {
1123
+ const idx = args.indexOf("--max-scorer-error-rate");
1124
+ if (idx === -1) return void 0;
1125
+ if (idx + 1 >= args.length) {
1126
+ console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
1127
+ process.exit(1);
1128
+ }
1129
+ const raw = args[idx + 1];
1130
+ const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
1131
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
1132
+ console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
1133
+ process.exit(1);
1134
+ }
1135
+ return n;
1136
+ }
1137
+ function scorerFilteredScorers(r) {
1138
+ const first = Array.isArray(r) ? r[0] : r;
1139
+ const meta = first?.metadata;
1140
+ if (!meta || meta.scorerFiltered !== true) return void 0;
1141
+ return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
1142
+ }
916
1143
  async function runCompare(args) {
917
1144
  const failOnRegression = args.includes("--fail-on-regression");
918
1145
  const thresholds = parseThresholdArg(args);
1146
+ const maxScorerErrorRate = parseMaxScorerErrorRate(args);
1147
+ const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
919
1148
  const files = args.filter(
920
- (a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1] === "--threshold")
1149
+ (a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
921
1150
  );
922
1151
  if (files.length !== 2) {
923
- console.error("Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>]");
1152
+ console.error(
1153
+ "Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
1154
+ );
924
1155
  process.exit(1);
925
1156
  }
926
1157
  const baselineRaw = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
927
1158
  const candidateRaw = JSON.parse(await (0, import_promises.readFile)(files[1], "utf-8"));
928
1159
  const baseline = baselineRaw;
929
1160
  const candidate = candidateRaw;
1161
+ const filteredSides = [
1162
+ ["baseline", baseline],
1163
+ ["candidate", candidate]
1164
+ ].filter(([, r]) => scorerFilteredScorers(r) != null);
1165
+ for (const [side, r] of filteredSides) {
1166
+ const ran = scorerFilteredScorers(r);
1167
+ const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
1168
+ console.error(
1169
+ `[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
1170
+ );
1171
+ }
1172
+ if (failOnRegression && filteredSides.length > 0) {
1173
+ refuseToGate(
1174
+ `${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
1175
+ );
1176
+ }
930
1177
  const compareOptions = thresholds != null ? { thresholds } : void 0;
931
1178
  const comparison = evalCompare(baseline, candidate, compareOptions);
932
1179
  console.log(
@@ -982,6 +1229,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
982
1229
  Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
983
1230
  `
984
1231
  );
1232
+ let sawThinnedSample = false;
1233
+ for (const name of scorerNames) {
1234
+ const s = comparison.scorers[name];
1235
+ const bFailed = s.baselineFailed ?? 0;
1236
+ const cFailed = s.candidateFailed ?? 0;
1237
+ if (bFailed > 0 || cFailed > 0) {
1238
+ sawThinnedSample = true;
1239
+ const bAtt = (s.baselineScored ?? 0) + bFailed;
1240
+ const cAtt = (s.candidateScored ?? 0) + cFailed;
1241
+ console.error(
1242
+ `[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
1243
+ );
1244
+ }
1245
+ }
1246
+ if (sawThinnedSample && maxScorerErrorRate == null) {
1247
+ console.error(
1248
+ `[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
1249
+ );
1250
+ }
1251
+ if (maxScorerErrorRate != null) {
1252
+ const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
1253
+ if (reason) refuseToGate(reason);
1254
+ }
985
1255
  if (failOnRegression && comparison.regressions.length > 0) {
986
1256
  const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
987
1257
  if (hasSignificance) {
@@ -994,12 +1264,19 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
994
1264
  }
995
1265
  }
996
1266
  }
997
- async function runRescore(args) {
998
- const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
1267
+ async function runRescore(args, signal) {
1268
+ const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
999
1269
  if (paths.length < 2) {
1000
1270
  console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
1001
1271
  process.exit(1);
1002
1272
  }
1273
+ if (scorerNames?.length) {
1274
+ console.error(
1275
+ "Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
1276
+ );
1277
+ process.exit(1);
1278
+ }
1279
+ const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
1003
1280
  const [resultsPath, evalFilePath] = paths;
1004
1281
  const raw = JSON.parse(await (0, import_promises.readFile)(resultsPath, "utf-8"));
1005
1282
  const results = Array.isArray(raw) ? raw : [raw];
@@ -1019,7 +1296,13 @@ async function runRescore(args) {
1019
1296
  }
1020
1297
  const rescored = [];
1021
1298
  for (const resultData of results) {
1022
- rescored.push(await rescore(resultData, evalConfig.scorers, runtime));
1299
+ if (signal.aborted) break;
1300
+ rescored.push(
1301
+ await rescore(resultData, evalConfig.scorers, runtime, {
1302
+ signal,
1303
+ ...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
1304
+ })
1305
+ );
1023
1306
  }
1024
1307
  for (const r of rescored) {
1025
1308
  console.log("\n" + formatTable(r) + "\n");
@@ -1069,14 +1352,15 @@ function formatTable(result) {
1069
1352
  lines.push(" " + "\u2500".repeat(ruleLen));
1070
1353
  for (const name of scorerNames) {
1071
1354
  const s = result.summary.scorers[name];
1072
- const validScoreCount = result.items.filter((i) => !i.error && i.scores[name] != null).length;
1073
- if (validScoreCount === 0) {
1355
+ const { scored, failed } = scorerSampleCounts(s, name, result.items);
1356
+ const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
1357
+ if (scored === 0) {
1074
1358
  lines.push(
1075
- ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
1359
+ ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
1076
1360
  );
1077
1361
  } else {
1078
1362
  lines.push(
1079
- ` ${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)}`
1363
+ ` ${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}`
1080
1364
  );
1081
1365
  }
1082
1366
  }
@@ -1124,8 +1408,10 @@ function formatMultiRunTable(summary) {
1124
1408
  for (const name of scorerNames) {
1125
1409
  const s = summary.scorers[name];
1126
1410
  const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
1411
+ const { scored, failed } = scorerSampleCounts(s, name);
1412
+ const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
1127
1413
  lines.push(
1128
- ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
1414
+ ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
1129
1415
  );
1130
1416
  }
1131
1417
  if (summary.timing) {
@@ -1139,6 +1425,26 @@ function formatMultiRunTable(summary) {
1139
1425
  lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
1140
1426
  return lines.join("\n");
1141
1427
  }
1428
+ function reportDegraded(result, label) {
1429
+ const d = result.summary.degraded;
1430
+ if (!d || d.length === 0) return false;
1431
+ for (const x of d) {
1432
+ const attempted = x.scored + x.failed;
1433
+ const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
1434
+ console.error(
1435
+ `[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
1436
+ );
1437
+ }
1438
+ return true;
1439
+ }
1440
+ function reportTotalWipeout(result, label) {
1441
+ const { count, failures } = result.summary;
1442
+ if (count === 0 || failures < count) return false;
1443
+ console.error(
1444
+ `[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
1445
+ );
1446
+ return true;
1447
+ }
1142
1448
  async function resolveRuntimeFromConfig(configPath) {
1143
1449
  try {
1144
1450
  const mod = await (0, import_axl.importModule)(configPath, import_meta.url);
@@ -1199,41 +1505,17 @@ async function getRuntime(configArg, conditions) {
1199
1505
  const { AxlRuntime } = await import("@axlsdk/axl");
1200
1506
  return new AxlRuntime();
1201
1507
  }
1202
- function parseEvalArgs(args) {
1203
- let outputPath;
1204
- let configArg;
1205
- let conditions = [];
1206
- let runs = 1;
1207
- let captureTraces = false;
1208
- const paths = [];
1209
- for (let i = 0; i < args.length; i++) {
1210
- const arg = args[i];
1211
- if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
1212
- if (i + 1 >= args.length) {
1213
- console.error(`Error: ${arg} requires a value`);
1214
- process.exit(1);
1215
- }
1216
- const value = args[++i];
1217
- if (arg === "--output") outputPath = value;
1218
- else if (arg === "--config") configArg = value;
1219
- else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
1220
- else
1221
- conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
1222
- } else if (arg === "--capture-traces") {
1223
- captureTraces = true;
1224
- } else if (arg.startsWith("--")) {
1225
- if (!KNOWN_FLAGS.has(arg)) {
1226
- console.error(`Unknown flag: ${arg}`);
1227
- process.exit(1);
1228
- }
1229
- } else {
1230
- paths.push(arg);
1231
- }
1232
- }
1233
- return { outputPath, configArg, conditions, runs, captureTraces, paths };
1234
- }
1235
- async function runEvalCommand(args) {
1236
- const { outputPath, configArg, conditions, runs, captureTraces, paths } = parseEvalArgs(args);
1508
+ async function runEvalCommand(args, signal) {
1509
+ const {
1510
+ outputPath,
1511
+ configArg,
1512
+ conditions,
1513
+ runs,
1514
+ captureTraces,
1515
+ concurrency,
1516
+ scorerNames,
1517
+ paths
1518
+ } = parseEvalArgs(args);
1237
1519
  if (paths.length === 0) {
1238
1520
  console.error("Error: No eval file path provided");
1239
1521
  process.exit(1);
@@ -1246,11 +1528,16 @@ async function runEvalCommand(args) {
1246
1528
  console.error("Error: No eval files found");
1247
1529
  process.exit(1);
1248
1530
  }
1531
+ if (scorerNames?.length && evalFiles.length > 1) {
1532
+ console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
1533
+ process.exit(1);
1534
+ }
1249
1535
  const runtime = await getRuntime(configArg, conditions);
1250
1536
  const results = [];
1251
1537
  let failedFiles = 0;
1252
1538
  try {
1253
1539
  for (const filePath of evalFiles) {
1540
+ if (signal.aborted) break;
1254
1541
  try {
1255
1542
  const mod = await (0, import_axl.importModule)(path.resolve(filePath), import_meta.url);
1256
1543
  const evalConfig = (0, import_axl.pickDefault)(mod);
@@ -1260,6 +1547,22 @@ async function runEvalCommand(args) {
1260
1547
  failedFiles++;
1261
1548
  continue;
1262
1549
  }
1550
+ evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
1551
+ if (scorerNames?.length) {
1552
+ const available = evalConfig.scorers.map((s) => s.name);
1553
+ const unknown = scorerNames.filter((n) => !available.includes(n));
1554
+ if (unknown.length) {
1555
+ console.error(
1556
+ `Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
1557
+ );
1558
+ process.exit(1);
1559
+ }
1560
+ evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
1561
+ if (evalConfig.scorers.length === 0) {
1562
+ console.error("Error: --scorers matched no scorers.");
1563
+ process.exit(1);
1564
+ }
1565
+ }
1263
1566
  const exported = (0, import_axl.pickExport)(mod, "executeWorkflow");
1264
1567
  let customExecute;
1265
1568
  if (typeof exported === "function") {
@@ -1315,18 +1618,27 @@ async function runEvalCommand(args) {
1315
1618
  failedFiles++;
1316
1619
  continue;
1317
1620
  }
1318
- const runOptions = captureTraces ? { captureTraces: true } : void 0;
1621
+ const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
1622
+ const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
1623
+ const stampFiltered = (result) => {
1624
+ if (scorersRun) {
1625
+ result.metadata.scorerFiltered = true;
1626
+ result.metadata.scorersRun = scorersRun;
1627
+ }
1628
+ };
1319
1629
  if (runs > 1) {
1320
1630
  const { randomUUID: randomUUID4 } = await import("crypto");
1321
1631
  const runGroupId = randomUUID4();
1322
1632
  const runResults = [];
1323
1633
  let runFailure;
1324
1634
  for (let r = 0; r < runs; r++) {
1635
+ if (signal.aborted) break;
1325
1636
  console.error(`[axl-eval] Run ${r + 1}/${runs}...`);
1326
1637
  try {
1327
1638
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1328
1639
  result.metadata.runGroupId = runGroupId;
1329
1640
  result.metadata.runIndex = r;
1641
+ stampFiltered(result);
1330
1642
  runResults.push(result);
1331
1643
  } catch (err) {
1332
1644
  runFailure = err instanceof Error ? err : new Error(String(err));
@@ -1358,10 +1670,21 @@ async function runEvalCommand(args) {
1358
1670
  const summary = aggregateRuns(runResults);
1359
1671
  console.log("\n" + formatMultiRunTable(summary) + "\n");
1360
1672
  for (const r of runResults) results.push(r);
1673
+ let anyFailing = false;
1674
+ for (const r of runResults) {
1675
+ const wipeout = reportTotalWipeout(r, filePath);
1676
+ const degraded = reportDegraded(r, filePath);
1677
+ if (wipeout || degraded) anyFailing = true;
1678
+ }
1679
+ if (anyFailing && !partial) failedFiles++;
1361
1680
  } else {
1362
1681
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1682
+ stampFiltered(result);
1363
1683
  results.push(result);
1364
1684
  console.log("\n" + formatTable(result) + "\n");
1685
+ const wipeout = reportTotalWipeout(result, filePath);
1686
+ const degraded = reportDegraded(result, filePath);
1687
+ if (wipeout || degraded) failedFiles++;
1365
1688
  }
1366
1689
  } catch (err) {
1367
1690
  console.error(