@axlsdk/eval 0.17.9 → 0.18.1

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,54 @@ 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
+ let skipped = 0;
115
+ for (const i of items) {
116
+ if (i.error) continue;
117
+ if (i.scores[name] != null) scored++;
118
+ else if (i.scoreDetails?.[name]?.skipped === true) skipped++;
119
+ else if (i.scoreDetails?.[name]?.duration != null) failed++;
120
+ }
121
+ return { scored, failed, skipped };
122
+ }
123
+ function evaluateScorerTolerance(scored, failed, type, limit) {
124
+ const attempted = scored + failed;
125
+ const rate = attempted === 0 ? 0 : failed / attempted;
126
+ const exceeds = attempted > 0 && (type === "deterministic" ? failed > 0 : rate > limit);
127
+ return { attempted, rate, exceeds, zeroSample: attempted === 0 };
128
+ }
129
+ async function mapWithConcurrency(items, concurrency, task) {
130
+ const results = new Array(items.length);
131
+ const requested = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;
132
+ const workerCount = items.length === 0 ? 0 : Math.min(Math.max(1, requested), items.length);
133
+ let next = 0;
134
+ async function runNext() {
135
+ while (next < items.length) {
136
+ const i = next++;
137
+ results[i] = await task(items[i], i);
138
+ }
139
+ }
140
+ await Promise.all(Array.from({ length: workerCount }, () => runNext()));
141
+ return results;
142
+ }
143
+
144
+ // src/compare.ts
101
145
  var DEFAULT_LLM_THRESHOLD = 0.05;
102
146
  var DEFAULT_DETERMINISTIC_THRESHOLD = 0;
103
147
  var LEGACY_THRESHOLD = 0.1;
@@ -171,19 +215,43 @@ function evalCompare(baseline, candidate, options) {
171
215
  const delta = round2(deltaRaw);
172
216
  const deltaPercent = bMeanRaw > 0 ? round2(deltaRaw / bMeanRaw * 100) : 0;
173
217
  const threshold = resolveThreshold(name, options, baselineRef.metadata);
218
+ let baselineScored = 0;
219
+ let baselineFailed = 0;
220
+ let baselineSkipped = 0;
221
+ for (const run of baselineRuns) {
222
+ const c = scorerCounts(run.items, name);
223
+ baselineScored += c.scored;
224
+ baselineFailed += c.failed;
225
+ baselineSkipped += c.skipped;
226
+ }
227
+ let candidateScored = 0;
228
+ let candidateFailed = 0;
229
+ let candidateSkipped = 0;
230
+ for (const run of candidateRuns) {
231
+ const c = scorerCounts(run.items, name);
232
+ candidateScored += c.scored;
233
+ candidateFailed += c.failed;
234
+ candidateSkipped += c.skipped;
235
+ }
174
236
  const entry = {
175
237
  baselineMean: bMean,
176
238
  candidateMean: cMean,
177
239
  delta,
178
- deltaPercent
240
+ deltaPercent,
241
+ baselineScored,
242
+ baselineFailed,
243
+ candidateScored,
244
+ candidateFailed,
245
+ baselineSkipped,
246
+ candidateSkipped
179
247
  };
180
248
  const diffs = pairedDiffs[name];
249
+ entry.n = diffs.length;
181
250
  if (diffs.length >= 2) {
182
251
  const ci = pairedBootstrapCI(diffs);
183
252
  entry.ci = { lower: ci.lower, upper: ci.upper };
184
253
  entry.pRegression = ci.pRegression;
185
254
  entry.pImprovement = ci.pImprovement;
186
- entry.n = diffs.length;
187
255
  const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
188
256
  entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
189
257
  }
@@ -311,25 +379,98 @@ function detectPartial(runs) {
311
379
  if (runs.length >= attempted) return void 0;
312
380
  return { completed: runs.length, attempted };
313
381
  }
382
+ function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
383
+ const scorerTypes = comparison.baseline.metadata?.scorerTypes ?? {};
384
+ for (const name of Object.keys(comparison.scorers)) {
385
+ const s = comparison.scorers[name];
386
+ const type = scorerTypes[name] === "deterministic" ? "deterministic" : "llm";
387
+ const limit = type === "deterministic" ? 0 : maxScorerErrorRate;
388
+ for (const side of ["baseline", "candidate"]) {
389
+ const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
390
+ const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
391
+ const skipped = (side === "baseline" ? s.baselineSkipped : s.candidateSkipped) ?? 0;
392
+ const verdict = evaluateScorerTolerance(scored, failed, type, limit);
393
+ if (verdict.zeroSample) {
394
+ const cause = skipped > 0 ? `every one of its items was N/A (the \`applies\` predicate skipped all ${skipped})` : `its items errored or produced no score`;
395
+ return `scorer "${name}" has no scored items on ${side} (${cause}) \u2014 cannot certify a mean over an empty sample.`;
396
+ }
397
+ if (verdict.exceeds) {
398
+ const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
399
+ return `scorer "${name}" failed ${(verdict.rate * 100).toFixed(1)}% on ${side} (${failed}/${verdict.attempted}), over the ${limitStr} limit.`;
400
+ }
401
+ }
402
+ }
403
+ return null;
404
+ }
314
405
 
315
406
  // src/scorer.ts
316
407
  function normalizeScorerResult(result) {
317
408
  return typeof result === "number" ? { score: result } : result;
318
409
  }
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) };
410
+ function extractScorerErrorCost(err) {
411
+ const v = err && typeof err === "object" ? err.cost : void 0;
412
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
330
413
  }
331
- function round3(n) {
332
- return Math.round(n * 1e3) / 1e3;
414
+
415
+ // src/score-item.ts
416
+ async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal) {
417
+ const scoreDetails = item.scoreDetails ??= {};
418
+ for (const s of scorers) {
419
+ item.scores[s.name] = null;
420
+ scoreDetails[s.name] = { score: null };
421
+ }
422
+ let itemScorerCost = 0;
423
+ const scorerErrorsByName = {};
424
+ await mapWithConcurrency(scorers, scorerConcurrency, async (scorer) => {
425
+ if (signal?.aborted) return;
426
+ const scorerStart = Date.now();
427
+ try {
428
+ if (scorer.applies && !scorer.applies(item.output, item.input, item.annotations)) {
429
+ item.scores[scorer.name] = null;
430
+ scoreDetails[scorer.name] = { score: null, skipped: true };
431
+ return;
432
+ }
433
+ const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
434
+ const scorerResult = normalizeScorerResult(raw);
435
+ if (scorerResult.cost != null) {
436
+ itemScorerCost += scorerResult.cost;
437
+ }
438
+ const scorerDuration = Date.now() - scorerStart;
439
+ if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
440
+ scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`;
441
+ item.scores[scorer.name] = null;
442
+ scoreDetails[scorer.name] = {
443
+ score: null,
444
+ metadata: scorerResult.metadata,
445
+ duration: scorerDuration,
446
+ cost: scorerResult.cost
447
+ };
448
+ } else {
449
+ item.scores[scorer.name] = round2(scorerResult.score);
450
+ scoreDetails[scorer.name] = {
451
+ score: round2(scorerResult.score),
452
+ metadata: scorerResult.metadata,
453
+ duration: scorerDuration,
454
+ cost: scorerResult.cost
455
+ };
456
+ }
457
+ } catch (err) {
458
+ if (err?.name === "AbortError") return;
459
+ const errCost = extractScorerErrorCost(err);
460
+ if (errCost != null) itemScorerCost += errCost;
461
+ scorerErrorsByName[scorer.name] = `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`;
462
+ item.scores[scorer.name] = null;
463
+ scoreDetails[scorer.name] = {
464
+ score: null,
465
+ duration: Date.now() - scorerStart,
466
+ cost: errCost
467
+ };
468
+ }
469
+ });
470
+ const orderedErrors = scorers.map((s) => scorerErrorsByName[s.name]).filter((e) => e != null);
471
+ if (orderedErrors.length > 0) item.scorerErrors = orderedErrors;
472
+ item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
473
+ return itemScorerCost;
333
474
  }
334
475
 
335
476
  // src/runner.ts
@@ -372,7 +513,9 @@ async function runEval(config, executeWorkflow, runtime, options) {
372
513
  const startTime = Date.now();
373
514
  const id = (0, import_node_crypto.randomUUID)();
374
515
  const items = await config.dataset.getItems();
516
+ const droppedAnnotationKeys = [...config.dataset.droppedAnnotationKeys ?? []];
375
517
  const concurrency = config.concurrency ?? 5;
518
+ const scorerConcurrency = config.scorerConcurrency ?? 5;
376
519
  const budgetLimit = config.budget ? parseCost(config.budget) : void 0;
377
520
  const scorerContext = {
378
521
  resolveProvider: (uri) => {
@@ -461,89 +604,53 @@ async function runEval(config, executeWorkflow, runtime, options) {
461
604
  if (budgetLimit != null && totalCost > budgetLimit) {
462
605
  budgetExceeded = true;
463
606
  }
464
- evalItem.scoreDetails = {};
465
- let itemScorerCost = 0;
466
- for (const scorer of config.scorers) {
467
- if (options?.signal?.aborted) break;
468
- const scorerStart = Date.now();
469
- try {
470
- const raw = await scorer.score(
471
- evalItem.output,
472
- item.input,
473
- item.annotations,
474
- scorerContext
475
- );
476
- const scorerResult = normalizeScorerResult(raw);
477
- if (scorerResult.cost != null) {
478
- itemScorerCost += scorerResult.cost;
479
- totalCost += scorerResult.cost;
480
- }
481
- const scorerDuration = Date.now() - scorerStart;
482
- if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
483
- if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
484
- evalItem.scorerErrors.push(
485
- `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
486
- );
487
- evalItem.scores[scorer.name] = null;
488
- evalItem.scoreDetails[scorer.name] = {
489
- score: null,
490
- metadata: scorerResult.metadata,
491
- duration: scorerDuration,
492
- cost: scorerResult.cost
493
- };
494
- } else {
495
- evalItem.scores[scorer.name] = round3(scorerResult.score);
496
- evalItem.scoreDetails[scorer.name] = {
497
- score: round3(scorerResult.score),
498
- metadata: scorerResult.metadata,
499
- duration: scorerDuration,
500
- cost: scorerResult.cost
501
- };
502
- }
503
- } catch (err) {
504
- const errCost = typeof err?.cost === "number" ? err.cost : void 0;
505
- if (errCost != null) {
506
- itemScorerCost += errCost;
507
- totalCost += errCost;
508
- }
509
- if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
510
- evalItem.scorerErrors.push(
511
- `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
512
- );
513
- evalItem.scores[scorer.name] = null;
514
- evalItem.scoreDetails[scorer.name] = {
515
- score: null,
516
- duration: Date.now() - scorerStart,
517
- cost: errCost
518
- };
519
- }
520
- }
521
- evalItem.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
607
+ const itemScorerCost = await scoreItem(
608
+ evalItem,
609
+ config.scorers,
610
+ scorerConcurrency,
611
+ scorerContext,
612
+ options?.signal
613
+ );
614
+ totalCost += itemScorerCost;
522
615
  evalItems[itemIndex] = evalItem;
523
616
  options?.onProgress?.({ type: "item_done", itemIndex, totalItems: items.length });
524
617
  }
525
- let index = 0;
526
- async function runNext() {
527
- while (index < items.length) {
528
- const currentIndex = index++;
529
- await processItem(items[currentIndex], currentIndex);
530
- }
531
- }
532
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());
533
- await Promise.all(workers);
618
+ await mapWithConcurrency(items, concurrency, (item, i) => processItem(item, i));
534
619
  const failures = evalItems.filter((i) => i.error).length;
535
620
  const scorerNames = config.scorers.map((s) => s.name);
536
621
  const scorerStats = {};
537
622
  for (const name of scorerNames) {
538
623
  const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
539
- scorerStats[name] = computeStats(scores);
624
+ const { scored, failed, skipped } = scorerCounts(evalItems, name);
625
+ scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
540
626
  }
541
- const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
542
- const timing = durations.length > 0 ? computeStats(durations) : void 0;
543
627
  const scorerTypes = {};
544
628
  for (const s of config.scorers) {
545
629
  scorerTypes[s.name] = s.isLlm ? "llm" : "deterministic";
546
630
  }
631
+ let degraded;
632
+ const rawLimit = config.failOnScorerErrorRate;
633
+ if (rawLimit != null) {
634
+ if (!Number.isFinite(rawLimit) || rawLimit < 0 || rawLimit > 1) {
635
+ console.warn(
636
+ `[axl-eval] Ignoring invalid failOnScorerErrorRate (${rawLimit}); expected a number in [0, 1].`
637
+ );
638
+ } else {
639
+ for (const name of scorerNames) {
640
+ const stats = scorerStats[name];
641
+ const scored = stats.scored ?? 0;
642
+ const failed = stats.failed ?? 0;
643
+ const type = scorerTypes[name] === "llm" ? "llm" : "deterministic";
644
+ const limit = type === "deterministic" ? 0 : rawLimit;
645
+ const verdict = evaluateScorerTolerance(scored, failed, type, limit);
646
+ if (verdict.exceeds) {
647
+ (degraded ??= []).push({ scorer: name, rate: verdict.rate, limit, type, scored, failed });
648
+ }
649
+ }
650
+ }
651
+ }
652
+ const durations = evalItems.filter((i) => !i.error && i.duration != null).map((i) => i.duration);
653
+ const timing = durations.length > 0 ? computeStats(durations) : void 0;
547
654
  const totalModelCalls = /* @__PURE__ */ new Map();
548
655
  for (const item of evalItems) {
549
656
  const itemCounts = item.metadata?.modelCallCounts;
@@ -602,13 +709,20 @@ async function runEval(config, executeWorkflow, runtime, options) {
602
709
  ...config.metadata,
603
710
  scorerTypes,
604
711
  ...modelsMeta,
605
- ...workflowsMeta
712
+ ...workflowsMeta,
713
+ ...droppedAnnotationKeys.length > 0 ? { droppedAnnotationKeys } : {}
606
714
  },
607
715
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
608
716
  totalCost,
609
717
  duration: Date.now() - startTime,
610
718
  items: evalItems,
611
- summary: { count: items.length, failures, scorers: scorerStats, timing }
719
+ summary: {
720
+ count: items.length,
721
+ failures,
722
+ scorers: scorerStats,
723
+ timing,
724
+ ...degraded ? { degraded } : {}
725
+ }
612
726
  };
613
727
  }
614
728
 
@@ -617,6 +731,7 @@ var import_node_crypto2 = require("crypto");
617
731
  async function rescore(result, scorers, runtime, options) {
618
732
  const startTime = Date.now();
619
733
  const concurrency = options?.concurrency ?? 5;
734
+ const scorerConcurrency = options?.scorerConcurrency ?? 5;
620
735
  const scorerContext = {
621
736
  resolveProvider: (uri) => {
622
737
  if (typeof runtime.resolveProvider !== "function") {
@@ -660,77 +775,24 @@ async function rescore(result, scorers, runtime, options) {
660
775
  scores: {},
661
776
  scoreDetails: {}
662
777
  };
663
- let itemScorerCost = 0;
664
- for (const scorer of scorers) {
665
- const scorerStart = Date.now();
666
- try {
667
- const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
668
- const scorerResult = normalizeScorerResult(raw);
669
- if (scorerResult.cost != null) {
670
- itemScorerCost += scorerResult.cost;
671
- totalCost += scorerResult.cost;
672
- }
673
- const scorerDuration = Date.now() - scorerStart;
674
- if (!Number.isFinite(scorerResult.score) || scorerResult.score < 0 || scorerResult.score > 1) {
675
- if (!item.scorerErrors) item.scorerErrors = [];
676
- item.scorerErrors.push(
677
- `Scorer "${scorer.name}" returned out-of-range score ${scorerResult.score} for input ${JSON.stringify(item.input)}`
678
- );
679
- item.scores[scorer.name] = null;
680
- item.scoreDetails[scorer.name] = {
681
- score: null,
682
- metadata: scorerResult.metadata,
683
- duration: scorerDuration,
684
- cost: scorerResult.cost
685
- };
686
- } else {
687
- item.scores[scorer.name] = round3(scorerResult.score);
688
- item.scoreDetails[scorer.name] = {
689
- score: round3(scorerResult.score),
690
- metadata: scorerResult.metadata,
691
- duration: scorerDuration,
692
- cost: scorerResult.cost
693
- };
694
- }
695
- } catch (err) {
696
- const errCost = typeof err?.cost === "number" ? err.cost : void 0;
697
- if (errCost != null) {
698
- itemScorerCost += errCost;
699
- totalCost += errCost;
700
- }
701
- if (!item.scorerErrors) item.scorerErrors = [];
702
- item.scorerErrors.push(
703
- `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
704
- );
705
- item.scores[scorer.name] = null;
706
- item.scoreDetails[scorer.name] = {
707
- score: null,
708
- duration: Date.now() - scorerStart,
709
- cost: errCost
710
- };
711
- }
712
- }
713
- item.scorerCost = itemScorerCost > 0 ? itemScorerCost : void 0;
778
+ const itemScorerCost = await scoreItem(
779
+ item,
780
+ scorers,
781
+ scorerConcurrency,
782
+ scorerContext,
783
+ options?.signal
784
+ );
785
+ totalCost += itemScorerCost;
714
786
  rescored[itemIndex] = item;
715
787
  }
716
- let index = 0;
717
- async function runNext() {
718
- while (index < result.items.length) {
719
- const currentIndex = index++;
720
- await rescoreItem(result.items[currentIndex], currentIndex);
721
- }
722
- }
723
- const workers = Array.from(
724
- { length: Math.min(concurrency, result.items.length) },
725
- () => runNext()
726
- );
727
- await Promise.all(workers);
788
+ await mapWithConcurrency(result.items, concurrency, (item, i) => rescoreItem(item, i));
728
789
  const failures = rescored.filter((i) => i.error).length;
729
790
  const scorerNames = scorers.map((s) => s.name);
730
791
  const scorerStats = {};
731
792
  for (const name of scorerNames) {
732
793
  const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
733
- scorerStats[name] = computeStats(scores);
794
+ const { scored, failed, skipped } = scorerCounts(rescored, name);
795
+ scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
734
796
  }
735
797
  const scorerTypes = {};
736
798
  for (const s of scorers) {
@@ -740,7 +802,11 @@ async function rescore(result, scorers, runtime, options) {
740
802
  id: (0, import_node_crypto2.randomUUID)(),
741
803
  dataset: result.dataset,
742
804
  metadata: (() => {
743
- const { runGroupId: _, runIndex: __, ...rest } = result.metadata;
805
+ const rest = { ...result.metadata };
806
+ delete rest.runGroupId;
807
+ delete rest.runIndex;
808
+ delete rest.scorerFiltered;
809
+ delete rest.scorersRun;
744
810
  const merged = {
745
811
  ...rest,
746
812
  rescored: true,
@@ -793,15 +859,26 @@ function aggregateRuns(runs) {
793
859
  const scorerNames = Object.keys(runs[0].summary.scorers);
794
860
  const scorers = {};
795
861
  for (const name of scorerNames) {
796
- const means = runs.map((r) => r.summary.scorers[name]?.mean ?? 0);
862
+ const contributing = runs.filter((r) => {
863
+ const sc = r.summary.scorers[name]?.scored;
864
+ return sc == null || sc > 0;
865
+ });
866
+ const meanSource = contributing.length > 0 ? contributing : runs;
867
+ const means = meanSource.map((r) => r.summary.scorers[name]?.mean ?? 0);
797
868
  const meanOfMeans = means.reduce((a, b) => a + b, 0) / means.length;
798
869
  const minMean = Math.min(...means);
799
870
  const maxMean = Math.max(...means);
871
+ const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
872
+ const failed = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.failed ?? 0), 0);
873
+ const skipped = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.skipped ?? 0), 0);
800
874
  scorers[name] = {
801
- mean: round3(meanOfMeans),
802
- std: round3(std(means, meanOfMeans)),
803
- min: round3(minMean),
804
- max: round3(maxMean)
875
+ mean: round2(meanOfMeans),
876
+ std: round2(std(means, meanOfMeans)),
877
+ min: round2(minMean),
878
+ max: round2(maxMean),
879
+ scored,
880
+ failed,
881
+ skipped
805
882
  };
806
883
  }
807
884
  let timing;
@@ -809,8 +886,8 @@ function aggregateRuns(runs) {
809
886
  if (timingMeans.length === runs.length && timingMeans.length > 0) {
810
887
  const meanTiming = timingMeans.reduce((a, b) => a + b, 0) / timingMeans.length;
811
888
  timing = {
812
- mean: round3(meanTiming),
813
- std: round3(std(timingMeans, meanTiming))
889
+ mean: round2(meanTiming),
890
+ std: round2(std(timingMeans, meanTiming))
814
891
  };
815
892
  }
816
893
  return {
@@ -818,8 +895,8 @@ function aggregateRuns(runs) {
818
895
  runCount: runs.length,
819
896
  workflows,
820
897
  dataset,
821
- totalCost: round3(totalCost),
822
- totalDuration: round3(totalDuration),
898
+ totalCost: round2(totalCost),
899
+ totalDuration: round2(totalDuration),
823
900
  scorers,
824
901
  timing
825
902
  };
@@ -861,11 +938,22 @@ function validateEvalConfig(cfg) {
861
938
  if (badScorerIdx !== -1) {
862
939
  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).`;
863
940
  }
941
+ for (const key of ["concurrency", "scorerConcurrency"]) {
942
+ const v = c[key];
943
+ if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
944
+ return `exports an invalid ${key} (${typeof v === "number" ? v : typeof v}) \u2014 must be a positive integer.`;
945
+ }
946
+ }
947
+ {
948
+ const v = c.failOnScorerErrorRate;
949
+ if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)) {
950
+ return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
951
+ }
952
+ }
864
953
  return void 0;
865
954
  }
866
955
 
867
- // src/cli.ts
868
- var import_meta = {};
956
+ // src/cli-args.ts
869
957
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
870
958
  "--output",
871
959
  "--config",
@@ -873,8 +961,99 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
873
961
  "--fail-on-regression",
874
962
  "--threshold",
875
963
  "--runs",
876
- "--capture-traces"
964
+ "--capture-traces",
965
+ "--concurrency",
966
+ "--scorers"
967
+ ]);
968
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
969
+ "--output",
970
+ "--config",
971
+ "--conditions",
972
+ "--runs",
973
+ "--concurrency",
974
+ "--scorers"
877
975
  ]);
976
+ function envInt(name) {
977
+ const raw = process.env[name];
978
+ if (raw == null || raw === "") return void 0;
979
+ const n = parseInt(raw, 10);
980
+ if (!Number.isFinite(n) || n <= 0) return void 0;
981
+ return n;
982
+ }
983
+ function parseEvalArgs(args) {
984
+ let outputPath;
985
+ let configArg;
986
+ let conditions = [];
987
+ let runs = 1;
988
+ let captureTraces = false;
989
+ let concurrency;
990
+ let scorerNames;
991
+ const paths = [];
992
+ for (let i = 0; i < args.length; i++) {
993
+ const arg = args[i];
994
+ if (VALUE_FLAGS.has(arg)) {
995
+ if (i + 1 >= args.length) {
996
+ console.error(`Error: ${arg} requires a value`);
997
+ process.exit(1);
998
+ }
999
+ const value = args[++i];
1000
+ if (arg === "--output") outputPath = value;
1001
+ else if (arg === "--config") configArg = value;
1002
+ else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
1003
+ else if (arg === "--concurrency") {
1004
+ const n = parseInt(value, 10);
1005
+ if (!Number.isFinite(n) || n <= 0) {
1006
+ console.error(`[axl-eval] Ignoring invalid --concurrency "${value}"; using 1.`);
1007
+ concurrency = 1;
1008
+ } else {
1009
+ concurrency = n;
1010
+ }
1011
+ } else if (arg === "--scorers") {
1012
+ scorerNames = [
1013
+ ...new Set(
1014
+ value.split(",").map((s) => s.trim()).filter(Boolean)
1015
+ )
1016
+ ];
1017
+ } else {
1018
+ conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
1019
+ }
1020
+ } else if (arg === "--capture-traces") {
1021
+ captureTraces = true;
1022
+ } else if (arg.startsWith("--")) {
1023
+ if (!KNOWN_FLAGS.has(arg)) {
1024
+ console.error(`Unknown flag: ${arg}`);
1025
+ process.exit(1);
1026
+ }
1027
+ } else {
1028
+ paths.push(arg);
1029
+ }
1030
+ }
1031
+ return {
1032
+ outputPath,
1033
+ configArg,
1034
+ conditions,
1035
+ runs,
1036
+ captureTraces,
1037
+ concurrency,
1038
+ scorerNames,
1039
+ paths
1040
+ };
1041
+ }
1042
+
1043
+ // src/cli.ts
1044
+ var import_meta = {};
1045
+ function refuseToGate(reason) {
1046
+ console.error(`[axl-eval] Refusing to gate: ${reason}`);
1047
+ process.exit(1);
1048
+ }
1049
+ function scorerSampleCounts(entry, name, items) {
1050
+ if (entry.scored != null && entry.failed != null) {
1051
+ const skipped = entry.skipped ?? (items ? scorerCounts(items, name).skipped : 0);
1052
+ return { scored: entry.scored, failed: entry.failed, skipped };
1053
+ }
1054
+ if (items) return scorerCounts(items, name);
1055
+ return { scored: entry.scored ?? 0, failed: entry.failed ?? 0, skipped: entry.skipped ?? 0 };
1056
+ }
878
1057
  function installAbortHandler(controller) {
879
1058
  let signalled = false;
880
1059
  const onSignal = (sig) => {
@@ -901,6 +1080,15 @@ Usage:
901
1080
  axl-eval <path> --output <file> Save results to JSON
902
1081
  axl-eval <path> --config <file> Use config file for runtime
903
1082
  axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
1083
+ axl-eval <path> --concurrency <n> Override item concurrency (flag > env
1084
+ AXL_EVAL_CONCURRENCY > config > 5).
1085
+ Max concurrent scorer calls is
1086
+ concurrency x scorerConcurrency (the
1087
+ per-eval scorerConcurrency defaults to 5).
1088
+ axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
1089
+ file only). Stamps the result as a
1090
+ subset so compare won't treat it as a
1091
+ full baseline.
904
1092
  axl-eval <path> --capture-traces Populate EvalItem.traces on every item
905
1093
  (success + failure). Adds memory
906
1094
  overhead proportional to dataset size
@@ -909,6 +1097,12 @@ Usage:
909
1097
  axl-eval compare <a> <b> Compare two eval result files
910
1098
  axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
911
1099
  axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
1100
+ axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
1101
+ failure rate exceeds the limit on
1102
+ either side (deterministic scorers
1103
+ tolerate zero failures). Always warns
1104
+ when either side rests on a thinned
1105
+ sample, even without this flag.
912
1106
 
913
1107
  Config auto-detection (when --config is not specified):
914
1108
  ${import_axl.CONFIG_CANDIDATES.join(" -> ")}
@@ -943,20 +1137,61 @@ function parseThresholdArg(args) {
943
1137
  }
944
1138
  return Object.keys(map).length > 0 ? map : void 0;
945
1139
  }
1140
+ function parseMaxScorerErrorRate(args) {
1141
+ const idx = args.indexOf("--max-scorer-error-rate");
1142
+ if (idx === -1) return void 0;
1143
+ if (idx + 1 >= args.length) {
1144
+ console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
1145
+ process.exit(1);
1146
+ }
1147
+ const raw = args[idx + 1];
1148
+ const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
1149
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
1150
+ console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
1151
+ process.exit(1);
1152
+ }
1153
+ return n;
1154
+ }
1155
+ function scorerFilteredScorers(r) {
1156
+ const first = Array.isArray(r) ? r[0] : r;
1157
+ const meta = first?.metadata;
1158
+ if (!meta || meta.scorerFiltered !== true) return void 0;
1159
+ return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
1160
+ }
946
1161
  async function runCompare(args) {
947
1162
  const failOnRegression = args.includes("--fail-on-regression");
948
1163
  const thresholds = parseThresholdArg(args);
1164
+ const maxScorerErrorRate = parseMaxScorerErrorRate(args);
1165
+ const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
949
1166
  const files = args.filter(
950
- (a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1] === "--threshold")
1167
+ (a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
951
1168
  );
952
1169
  if (files.length !== 2) {
953
- console.error("Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>]");
1170
+ console.error(
1171
+ "Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
1172
+ );
954
1173
  process.exit(1);
955
1174
  }
956
1175
  const baselineRaw = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
957
1176
  const candidateRaw = JSON.parse(await (0, import_promises.readFile)(files[1], "utf-8"));
958
1177
  const baseline = baselineRaw;
959
1178
  const candidate = candidateRaw;
1179
+ const filteredSides = [
1180
+ ["baseline", baseline],
1181
+ ["candidate", candidate]
1182
+ ].filter(([, r]) => scorerFilteredScorers(r) != null);
1183
+ for (const [side, r] of filteredSides) {
1184
+ const ran = scorerFilteredScorers(r);
1185
+ const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
1186
+ console.error(
1187
+ `[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
1188
+ );
1189
+ }
1190
+ if (failOnRegression && filteredSides.length > 0) {
1191
+ refuseToGate(
1192
+ `${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
1193
+ );
1194
+ }
960
1195
  const compareOptions = thresholds != null ? { thresholds } : void 0;
961
1196
  const comparison = evalCompare(baseline, candidate, compareOptions);
962
1197
  console.log(
@@ -1012,6 +1247,39 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
1012
1247
  Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
1013
1248
  `
1014
1249
  );
1250
+ let sawThinnedSample = false;
1251
+ for (const name of scorerNames) {
1252
+ const s = comparison.scorers[name];
1253
+ const bFailed = s.baselineFailed ?? 0;
1254
+ const cFailed = s.candidateFailed ?? 0;
1255
+ if (bFailed > 0 || cFailed > 0) {
1256
+ sawThinnedSample = true;
1257
+ const bAtt = (s.baselineScored ?? 0) + bFailed;
1258
+ const cAtt = (s.candidateScored ?? 0) + cFailed;
1259
+ console.error(
1260
+ `[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
1261
+ );
1262
+ }
1263
+ const bSkipped = s.baselineSkipped ?? 0;
1264
+ const cSkipped = s.candidateSkipped ?? 0;
1265
+ const pairedN = s.n ?? 0;
1266
+ const minScored = Math.min(s.baselineScored ?? 0, s.candidateScored ?? 0);
1267
+ const skipsDiverge = (bSkipped > 0 || cSkipped > 0) && (bSkipped !== cSkipped || pairedN < minScored);
1268
+ if (skipsDiverge) {
1269
+ console.error(
1270
+ `[axl-eval] NOTE: scorer "${name}" applies to different subsets per side (baseline ${bSkipped} N/A, candidate ${cSkipped} N/A; ${pairedN} item(s) scored on both); the two means cover different applicable subsets, so compare the delta with care.`
1271
+ );
1272
+ }
1273
+ }
1274
+ if (sawThinnedSample && maxScorerErrorRate == null) {
1275
+ console.error(
1276
+ `[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
1277
+ );
1278
+ }
1279
+ if (maxScorerErrorRate != null) {
1280
+ const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
1281
+ if (reason) refuseToGate(reason);
1282
+ }
1015
1283
  if (failOnRegression && comparison.regressions.length > 0) {
1016
1284
  const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
1017
1285
  if (hasSignificance) {
@@ -1025,11 +1293,18 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
1025
1293
  }
1026
1294
  }
1027
1295
  async function runRescore(args, signal) {
1028
- const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
1296
+ const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
1029
1297
  if (paths.length < 2) {
1030
1298
  console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
1031
1299
  process.exit(1);
1032
1300
  }
1301
+ if (scorerNames?.length) {
1302
+ console.error(
1303
+ "Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
1304
+ );
1305
+ process.exit(1);
1306
+ }
1307
+ const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
1033
1308
  const [resultsPath, evalFilePath] = paths;
1034
1309
  const raw = JSON.parse(await (0, import_promises.readFile)(resultsPath, "utf-8"));
1035
1310
  const results = Array.isArray(raw) ? raw : [raw];
@@ -1050,7 +1325,12 @@ async function runRescore(args, signal) {
1050
1325
  const rescored = [];
1051
1326
  for (const resultData of results) {
1052
1327
  if (signal.aborted) break;
1053
- rescored.push(await rescore(resultData, evalConfig.scorers, runtime, { signal }));
1328
+ rescored.push(
1329
+ await rescore(resultData, evalConfig.scorers, runtime, {
1330
+ signal,
1331
+ ...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
1332
+ })
1333
+ );
1054
1334
  }
1055
1335
  for (const r of rescored) {
1056
1336
  console.log("\n" + formatTable(r) + "\n");
@@ -1100,14 +1380,18 @@ function formatTable(result) {
1100
1380
  lines.push(" " + "\u2500".repeat(ruleLen));
1101
1381
  for (const name of scorerNames) {
1102
1382
  const s = result.summary.scorers[name];
1103
- const validScoreCount = result.items.filter((i) => !i.error && i.scores[name] != null).length;
1104
- if (validScoreCount === 0) {
1383
+ const { scored, failed, skipped } = scorerSampleCounts(s, name, result.items);
1384
+ const annotationParts = [];
1385
+ if (failed > 0) annotationParts.push(`${scored}/${scored + failed} scored, ${failed} failed`);
1386
+ if (skipped > 0) annotationParts.push(`${skipped} N/A`);
1387
+ const annotation = annotationParts.length > 0 ? ` (${annotationParts.join(", ")})` : "";
1388
+ if (scored === 0) {
1105
1389
  lines.push(
1106
- ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
1390
+ ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
1107
1391
  );
1108
1392
  } else {
1109
1393
  lines.push(
1110
- ` ${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)}`
1394
+ ` ${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}`
1111
1395
  );
1112
1396
  }
1113
1397
  }
@@ -1155,8 +1439,10 @@ function formatMultiRunTable(summary) {
1155
1439
  for (const name of scorerNames) {
1156
1440
  const s = summary.scorers[name];
1157
1441
  const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
1442
+ const { scored, failed } = scorerSampleCounts(s, name);
1443
+ const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
1158
1444
  lines.push(
1159
- ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
1445
+ ` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
1160
1446
  );
1161
1447
  }
1162
1448
  if (summary.timing) {
@@ -1170,6 +1456,36 @@ function formatMultiRunTable(summary) {
1170
1456
  lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
1171
1457
  return lines.join("\n");
1172
1458
  }
1459
+ function reportDegraded(result, label) {
1460
+ const d = result.summary.degraded;
1461
+ if (!d || d.length === 0) return false;
1462
+ for (const x of d) {
1463
+ const attempted = x.scored + x.failed;
1464
+ const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
1465
+ console.error(
1466
+ `[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
1467
+ );
1468
+ }
1469
+ return true;
1470
+ }
1471
+ function reportFullySkippedScorers(result, label) {
1472
+ for (const [name, entry] of Object.entries(result.summary.scorers)) {
1473
+ const { scored, failed, skipped } = scorerSampleCounts(entry, name, result.items);
1474
+ if (skipped > 0 && scored + failed === 0) {
1475
+ console.error(
1476
+ `[axl-eval] NOTE: ${label} scorer "${name}" was not applicable to any item (${skipped} N/A, 0 scored) \u2014 its mean is not meaningful. Check the scorer's \`applies\` predicate and dataset if you expected it to run.`
1477
+ );
1478
+ }
1479
+ }
1480
+ }
1481
+ function reportTotalWipeout(result, label) {
1482
+ const { count, failures } = result.summary;
1483
+ if (count === 0 || failures < count) return false;
1484
+ console.error(
1485
+ `[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
1486
+ );
1487
+ return true;
1488
+ }
1173
1489
  async function resolveRuntimeFromConfig(configPath) {
1174
1490
  try {
1175
1491
  const mod = await (0, import_axl.importModule)(configPath, import_meta.url);
@@ -1230,41 +1546,17 @@ async function getRuntime(configArg, conditions) {
1230
1546
  const { AxlRuntime } = await import("@axlsdk/axl");
1231
1547
  return new AxlRuntime();
1232
1548
  }
1233
- function parseEvalArgs(args) {
1234
- let outputPath;
1235
- let configArg;
1236
- let conditions = [];
1237
- let runs = 1;
1238
- let captureTraces = false;
1239
- const paths = [];
1240
- for (let i = 0; i < args.length; i++) {
1241
- const arg = args[i];
1242
- if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
1243
- if (i + 1 >= args.length) {
1244
- console.error(`Error: ${arg} requires a value`);
1245
- process.exit(1);
1246
- }
1247
- const value = args[++i];
1248
- if (arg === "--output") outputPath = value;
1249
- else if (arg === "--config") configArg = value;
1250
- else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
1251
- else
1252
- conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
1253
- } else if (arg === "--capture-traces") {
1254
- captureTraces = true;
1255
- } else if (arg.startsWith("--")) {
1256
- if (!KNOWN_FLAGS.has(arg)) {
1257
- console.error(`Unknown flag: ${arg}`);
1258
- process.exit(1);
1259
- }
1260
- } else {
1261
- paths.push(arg);
1262
- }
1263
- }
1264
- return { outputPath, configArg, conditions, runs, captureTraces, paths };
1265
- }
1266
1549
  async function runEvalCommand(args, signal) {
1267
- const { outputPath, configArg, conditions, runs, captureTraces, paths } = parseEvalArgs(args);
1550
+ const {
1551
+ outputPath,
1552
+ configArg,
1553
+ conditions,
1554
+ runs,
1555
+ captureTraces,
1556
+ concurrency,
1557
+ scorerNames,
1558
+ paths
1559
+ } = parseEvalArgs(args);
1268
1560
  if (paths.length === 0) {
1269
1561
  console.error("Error: No eval file path provided");
1270
1562
  process.exit(1);
@@ -1277,6 +1569,10 @@ async function runEvalCommand(args, signal) {
1277
1569
  console.error("Error: No eval files found");
1278
1570
  process.exit(1);
1279
1571
  }
1572
+ if (scorerNames?.length && evalFiles.length > 1) {
1573
+ console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
1574
+ process.exit(1);
1575
+ }
1280
1576
  const runtime = await getRuntime(configArg, conditions);
1281
1577
  const results = [];
1282
1578
  let failedFiles = 0;
@@ -1292,6 +1588,22 @@ async function runEvalCommand(args, signal) {
1292
1588
  failedFiles++;
1293
1589
  continue;
1294
1590
  }
1591
+ evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
1592
+ if (scorerNames?.length) {
1593
+ const available = evalConfig.scorers.map((s) => s.name);
1594
+ const unknown = scorerNames.filter((n) => !available.includes(n));
1595
+ if (unknown.length) {
1596
+ console.error(
1597
+ `Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
1598
+ );
1599
+ process.exit(1);
1600
+ }
1601
+ evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
1602
+ if (evalConfig.scorers.length === 0) {
1603
+ console.error("Error: --scorers matched no scorers.");
1604
+ process.exit(1);
1605
+ }
1606
+ }
1295
1607
  const exported = (0, import_axl.pickExport)(mod, "executeWorkflow");
1296
1608
  let customExecute;
1297
1609
  if (typeof exported === "function") {
@@ -1348,6 +1660,13 @@ async function runEvalCommand(args, signal) {
1348
1660
  continue;
1349
1661
  }
1350
1662
  const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
1663
+ const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
1664
+ const stampFiltered = (result) => {
1665
+ if (scorersRun) {
1666
+ result.metadata.scorerFiltered = true;
1667
+ result.metadata.scorersRun = scorersRun;
1668
+ }
1669
+ };
1351
1670
  if (runs > 1) {
1352
1671
  const { randomUUID: randomUUID4 } = await import("crypto");
1353
1672
  const runGroupId = randomUUID4();
@@ -1360,6 +1679,7 @@ async function runEvalCommand(args, signal) {
1360
1679
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1361
1680
  result.metadata.runGroupId = runGroupId;
1362
1681
  result.metadata.runIndex = r;
1682
+ stampFiltered(result);
1363
1683
  runResults.push(result);
1364
1684
  } catch (err) {
1365
1685
  runFailure = err instanceof Error ? err : new Error(String(err));
@@ -1391,10 +1711,23 @@ async function runEvalCommand(args, signal) {
1391
1711
  const summary = aggregateRuns(runResults);
1392
1712
  console.log("\n" + formatMultiRunTable(summary) + "\n");
1393
1713
  for (const r of runResults) results.push(r);
1714
+ let anyFailing = false;
1715
+ for (const r of runResults) {
1716
+ const wipeout = reportTotalWipeout(r, filePath);
1717
+ const degraded = reportDegraded(r, filePath);
1718
+ reportFullySkippedScorers(r, filePath);
1719
+ if (wipeout || degraded) anyFailing = true;
1720
+ }
1721
+ if (anyFailing && !partial) failedFiles++;
1394
1722
  } else {
1395
1723
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1724
+ stampFiltered(result);
1396
1725
  results.push(result);
1397
1726
  console.log("\n" + formatTable(result) + "\n");
1727
+ const wipeout = reportTotalWipeout(result, filePath);
1728
+ const degraded = reportDegraded(result, filePath);
1729
+ reportFullySkippedScorers(result, filePath);
1730
+ if (wipeout || degraded) failedFiles++;
1398
1731
  }
1399
1732
  } catch (err) {
1400
1733
  console.error(