@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/README.md +127 -19
- package/dist/{chunk-NETD4AUB.js → chunk-66EYSYQS.js} +251 -164
- package/dist/chunk-66EYSYQS.js.map +1 -0
- package/dist/cli.cjs +541 -208
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +322 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -2
- package/dist/index.d.ts +254 -2
- package/dist/index.js +80 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -94,10 +94,54 @@ function xorshift32(seed) {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// src/
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
802
|
-
std:
|
|
803
|
-
min:
|
|
804
|
-
max:
|
|
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:
|
|
813
|
-
std:
|
|
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:
|
|
822
|
-
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]
|
|
1167
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
951
1168
|
);
|
|
952
1169
|
if (files.length !== 2) {
|
|
953
|
-
console.error(
|
|
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(
|
|
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
|
|
1104
|
-
|
|
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 {
|
|
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(
|