@axlsdk/eval 0.18.0 → 0.18.2

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
@@ -111,12 +111,14 @@ function round2(n) {
111
111
  function scorerCounts(items, name) {
112
112
  let scored = 0;
113
113
  let failed = 0;
114
+ let skipped = 0;
114
115
  for (const i of items) {
115
116
  if (i.error) continue;
116
117
  if (i.scores[name] != null) scored++;
118
+ else if (i.scoreDetails?.[name]?.skipped === true) skipped++;
117
119
  else if (i.scoreDetails?.[name]?.duration != null) failed++;
118
120
  }
119
- return { scored, failed };
121
+ return { scored, failed, skipped };
120
122
  }
121
123
  function evaluateScorerTolerance(scored, failed, type, limit) {
122
124
  const attempted = scored + failed;
@@ -215,17 +217,21 @@ function evalCompare(baseline, candidate, options) {
215
217
  const threshold = resolveThreshold(name, options, baselineRef.metadata);
216
218
  let baselineScored = 0;
217
219
  let baselineFailed = 0;
220
+ let baselineSkipped = 0;
218
221
  for (const run of baselineRuns) {
219
222
  const c = scorerCounts(run.items, name);
220
223
  baselineScored += c.scored;
221
224
  baselineFailed += c.failed;
225
+ baselineSkipped += c.skipped;
222
226
  }
223
227
  let candidateScored = 0;
224
228
  let candidateFailed = 0;
229
+ let candidateSkipped = 0;
225
230
  for (const run of candidateRuns) {
226
231
  const c = scorerCounts(run.items, name);
227
232
  candidateScored += c.scored;
228
233
  candidateFailed += c.failed;
234
+ candidateSkipped += c.skipped;
229
235
  }
230
236
  const entry = {
231
237
  baselineMean: bMean,
@@ -235,15 +241,17 @@ function evalCompare(baseline, candidate, options) {
235
241
  baselineScored,
236
242
  baselineFailed,
237
243
  candidateScored,
238
- candidateFailed
244
+ candidateFailed,
245
+ baselineSkipped,
246
+ candidateSkipped
239
247
  };
240
248
  const diffs = pairedDiffs[name];
249
+ entry.n = diffs.length;
241
250
  if (diffs.length >= 2) {
242
251
  const ci = pairedBootstrapCI(diffs);
243
252
  entry.ci = { lower: ci.lower, upper: ci.upper };
244
253
  entry.pRegression = ci.pRegression;
245
254
  entry.pImprovement = ci.pImprovement;
246
- entry.n = diffs.length;
247
255
  const ciExcludesZero = ci.lower > 0 || ci.upper < 0;
248
256
  entry.significant = ciExcludesZero && Math.abs(ci.mean) >= threshold;
249
257
  }
@@ -380,9 +388,11 @@ function evaluateScorerErrorRateGate(comparison, maxScorerErrorRate) {
380
388
  for (const side of ["baseline", "candidate"]) {
381
389
  const scored = (side === "baseline" ? s.baselineScored : s.candidateScored) ?? 0;
382
390
  const failed = (side === "baseline" ? s.baselineFailed : s.candidateFailed) ?? 0;
391
+ const skipped = (side === "baseline" ? s.baselineSkipped : s.candidateSkipped) ?? 0;
383
392
  const verdict = evaluateScorerTolerance(scored, failed, type, limit);
384
393
  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.`;
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.`;
386
396
  }
387
397
  if (verdict.exceeds) {
388
398
  const limitStr = type === "deterministic" ? "deterministic zero-tolerance" : `${(limit * 100).toFixed(1)}%`;
@@ -415,6 +425,11 @@ async function scoreItem(item, scorers, scorerConcurrency, scorerContext, signal
415
425
  if (signal?.aborted) return;
416
426
  const scorerStart = Date.now();
417
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
+ }
418
433
  const raw = await scorer.score(item.output, item.input, item.annotations, scorerContext);
419
434
  const scorerResult = normalizeScorerResult(raw);
420
435
  if (scorerResult.cost != null) {
@@ -606,8 +621,8 @@ async function runEval(config, executeWorkflow, runtime, options) {
606
621
  const scorerStats = {};
607
622
  for (const name of scorerNames) {
608
623
  const scores = evalItems.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
609
- const { scored, failed } = scorerCounts(evalItems, name);
610
- scorerStats[name] = { ...computeStats(scores), scored, failed };
624
+ const { scored, failed, skipped } = scorerCounts(evalItems, name);
625
+ scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
611
626
  }
612
627
  const scorerTypes = {};
613
628
  for (const s of config.scorers) {
@@ -776,8 +791,8 @@ async function rescore(result, scorers, runtime, options) {
776
791
  const scorerStats = {};
777
792
  for (const name of scorerNames) {
778
793
  const scores = rescored.filter((i) => !i.error && i.scores[name] != null).map((i) => i.scores[name]);
779
- const { scored, failed } = scorerCounts(rescored, name);
780
- scorerStats[name] = { ...computeStats(scores), scored, failed };
794
+ const { scored, failed, skipped } = scorerCounts(rescored, name);
795
+ scorerStats[name] = { ...computeStats(scores), scored, failed, skipped };
781
796
  }
782
797
  const scorerTypes = {};
783
798
  for (const s of scorers) {
@@ -855,13 +870,15 @@ function aggregateRuns(runs) {
855
870
  const maxMean = Math.max(...means);
856
871
  const scored = runs.reduce((sum, r) => sum + (r.summary.scorers[name]?.scored ?? 0), 0);
857
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);
858
874
  scorers[name] = {
859
875
  mean: round2(meanOfMeans),
860
876
  std: round2(std(means, meanOfMeans)),
861
877
  min: round2(minMean),
862
878
  max: round2(maxMean),
863
879
  scored,
864
- failed
880
+ failed,
881
+ skipped
865
882
  };
866
883
  }
867
884
  let timing;
@@ -1031,10 +1048,11 @@ function refuseToGate(reason) {
1031
1048
  }
1032
1049
  function scorerSampleCounts(entry, name, items) {
1033
1050
  if (entry.scored != null && entry.failed != null) {
1034
- return { scored: entry.scored, failed: entry.failed };
1051
+ const skipped = entry.skipped ?? (items ? scorerCounts(items, name).skipped : 0);
1052
+ return { scored: entry.scored, failed: entry.failed, skipped };
1035
1053
  }
1036
1054
  if (items) return scorerCounts(items, name);
1037
- return { scored: entry.scored ?? 0, failed: entry.failed ?? 0 };
1055
+ return { scored: entry.scored ?? 0, failed: entry.failed ?? 0, skipped: entry.skipped ?? 0 };
1038
1056
  }
1039
1057
  function installAbortHandler(controller) {
1040
1058
  let signalled = false;
@@ -1242,6 +1260,16 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
1242
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.`
1243
1261
  );
1244
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
+ }
1245
1273
  }
1246
1274
  if (sawThinnedSample && maxScorerErrorRate == null) {
1247
1275
  console.error(
@@ -1352,8 +1380,11 @@ function formatTable(result) {
1352
1380
  lines.push(" " + "\u2500".repeat(ruleLen));
1353
1381
  for (const name of scorerNames) {
1354
1382
  const s = result.summary.scorers[name];
1355
- const { scored, failed } = scorerSampleCounts(s, name, result.items);
1356
- const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
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(", ")})` : "";
1357
1388
  if (scored === 0) {
1358
1389
  lines.push(
1359
1390
  ` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
@@ -1437,6 +1468,16 @@ function reportDegraded(result, label) {
1437
1468
  }
1438
1469
  return true;
1439
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
+ }
1440
1481
  function reportTotalWipeout(result, label) {
1441
1482
  const { count, failures } = result.summary;
1442
1483
  if (count === 0 || failures < count) return false;
@@ -1674,6 +1715,7 @@ async function runEvalCommand(args, signal) {
1674
1715
  for (const r of runResults) {
1675
1716
  const wipeout = reportTotalWipeout(r, filePath);
1676
1717
  const degraded = reportDegraded(r, filePath);
1718
+ reportFullySkippedScorers(r, filePath);
1677
1719
  if (wipeout || degraded) anyFailing = true;
1678
1720
  }
1679
1721
  if (anyFailing && !partial) failedFiles++;
@@ -1684,6 +1726,7 @@ async function runEvalCommand(args, signal) {
1684
1726
  console.log("\n" + formatTable(result) + "\n");
1685
1727
  const wipeout = reportTotalWipeout(result, filePath);
1686
1728
  const degraded = reportDegraded(result, filePath);
1729
+ reportFullySkippedScorers(result, filePath);
1687
1730
  if (wipeout || degraded) failedFiles++;
1688
1731
  }
1689
1732
  } catch (err) {