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