@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.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
aggregateRuns,
|
|
4
4
|
evalCompare,
|
|
5
|
+
evaluateScorerErrorRateGate,
|
|
5
6
|
rescore,
|
|
6
|
-
runEval
|
|
7
|
-
|
|
7
|
+
runEval,
|
|
8
|
+
scorerCounts
|
|
9
|
+
} from "./chunk-66EYSYQS.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,98 @@ 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
|
+
const skipped = entry.skipped ?? (items ? scorerCounts(items, name).skipped : 0);
|
|
172
|
+
return { scored: entry.scored, failed: entry.failed, skipped };
|
|
173
|
+
}
|
|
174
|
+
if (items) return scorerCounts(items, name);
|
|
175
|
+
return { scored: entry.scored ?? 0, failed: entry.failed ?? 0, skipped: entry.skipped ?? 0 };
|
|
176
|
+
}
|
|
73
177
|
function installAbortHandler(controller) {
|
|
74
178
|
let signalled = false;
|
|
75
179
|
const onSignal = (sig) => {
|
|
@@ -96,6 +200,15 @@ Usage:
|
|
|
96
200
|
axl-eval <path> --output <file> Save results to JSON
|
|
97
201
|
axl-eval <path> --config <file> Use config file for runtime
|
|
98
202
|
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
203
|
+
axl-eval <path> --concurrency <n> Override item concurrency (flag > env
|
|
204
|
+
AXL_EVAL_CONCURRENCY > config > 5).
|
|
205
|
+
Max concurrent scorer calls is
|
|
206
|
+
concurrency x scorerConcurrency (the
|
|
207
|
+
per-eval scorerConcurrency defaults to 5).
|
|
208
|
+
axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
|
|
209
|
+
file only). Stamps the result as a
|
|
210
|
+
subset so compare won't treat it as a
|
|
211
|
+
full baseline.
|
|
99
212
|
axl-eval <path> --capture-traces Populate EvalItem.traces on every item
|
|
100
213
|
(success + failure). Adds memory
|
|
101
214
|
overhead proportional to dataset size
|
|
@@ -104,6 +217,12 @@ Usage:
|
|
|
104
217
|
axl-eval compare <a> <b> Compare two eval result files
|
|
105
218
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
106
219
|
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
220
|
+
axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
|
|
221
|
+
failure rate exceeds the limit on
|
|
222
|
+
either side (deterministic scorers
|
|
223
|
+
tolerate zero failures). Always warns
|
|
224
|
+
when either side rests on a thinned
|
|
225
|
+
sample, even without this flag.
|
|
107
226
|
|
|
108
227
|
Config auto-detection (when --config is not specified):
|
|
109
228
|
${CONFIG_CANDIDATES.join(" -> ")}
|
|
@@ -138,20 +257,61 @@ function parseThresholdArg(args) {
|
|
|
138
257
|
}
|
|
139
258
|
return Object.keys(map).length > 0 ? map : void 0;
|
|
140
259
|
}
|
|
260
|
+
function parseMaxScorerErrorRate(args) {
|
|
261
|
+
const idx = args.indexOf("--max-scorer-error-rate");
|
|
262
|
+
if (idx === -1) return void 0;
|
|
263
|
+
if (idx + 1 >= args.length) {
|
|
264
|
+
console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const raw = args[idx + 1];
|
|
268
|
+
const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
|
|
269
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
270
|
+
console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
return n;
|
|
274
|
+
}
|
|
275
|
+
function scorerFilteredScorers(r) {
|
|
276
|
+
const first = Array.isArray(r) ? r[0] : r;
|
|
277
|
+
const meta = first?.metadata;
|
|
278
|
+
if (!meta || meta.scorerFiltered !== true) return void 0;
|
|
279
|
+
return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
|
|
280
|
+
}
|
|
141
281
|
async function runCompare(args) {
|
|
142
282
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
143
283
|
const thresholds = parseThresholdArg(args);
|
|
284
|
+
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
285
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
|
|
144
286
|
const files = args.filter(
|
|
145
|
-
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1]
|
|
287
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
146
288
|
);
|
|
147
289
|
if (files.length !== 2) {
|
|
148
|
-
console.error(
|
|
290
|
+
console.error(
|
|
291
|
+
"Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
|
|
292
|
+
);
|
|
149
293
|
process.exit(1);
|
|
150
294
|
}
|
|
151
295
|
const baselineRaw = JSON.parse(await readFileAsync(files[0], "utf-8"));
|
|
152
296
|
const candidateRaw = JSON.parse(await readFileAsync(files[1], "utf-8"));
|
|
153
297
|
const baseline = baselineRaw;
|
|
154
298
|
const candidate = candidateRaw;
|
|
299
|
+
const filteredSides = [
|
|
300
|
+
["baseline", baseline],
|
|
301
|
+
["candidate", candidate]
|
|
302
|
+
].filter(([, r]) => scorerFilteredScorers(r) != null);
|
|
303
|
+
for (const [side, r] of filteredSides) {
|
|
304
|
+
const ran = scorerFilteredScorers(r);
|
|
305
|
+
const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
|
|
306
|
+
console.error(
|
|
307
|
+
`[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (failOnRegression && filteredSides.length > 0) {
|
|
311
|
+
refuseToGate(
|
|
312
|
+
`${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
155
315
|
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
156
316
|
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
157
317
|
console.log(
|
|
@@ -207,6 +367,39 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
207
367
|
Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
|
|
208
368
|
`
|
|
209
369
|
);
|
|
370
|
+
let sawThinnedSample = false;
|
|
371
|
+
for (const name of scorerNames) {
|
|
372
|
+
const s = comparison.scorers[name];
|
|
373
|
+
const bFailed = s.baselineFailed ?? 0;
|
|
374
|
+
const cFailed = s.candidateFailed ?? 0;
|
|
375
|
+
if (bFailed > 0 || cFailed > 0) {
|
|
376
|
+
sawThinnedSample = true;
|
|
377
|
+
const bAtt = (s.baselineScored ?? 0) + bFailed;
|
|
378
|
+
const cAtt = (s.candidateScored ?? 0) + cFailed;
|
|
379
|
+
console.error(
|
|
380
|
+
`[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
const bSkipped = s.baselineSkipped ?? 0;
|
|
384
|
+
const cSkipped = s.candidateSkipped ?? 0;
|
|
385
|
+
const pairedN = s.n ?? 0;
|
|
386
|
+
const minScored = Math.min(s.baselineScored ?? 0, s.candidateScored ?? 0);
|
|
387
|
+
const skipsDiverge = (bSkipped > 0 || cSkipped > 0) && (bSkipped !== cSkipped || pairedN < minScored);
|
|
388
|
+
if (skipsDiverge) {
|
|
389
|
+
console.error(
|
|
390
|
+
`[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.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (sawThinnedSample && maxScorerErrorRate == null) {
|
|
395
|
+
console.error(
|
|
396
|
+
`[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (maxScorerErrorRate != null) {
|
|
400
|
+
const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
|
|
401
|
+
if (reason) refuseToGate(reason);
|
|
402
|
+
}
|
|
210
403
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
211
404
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
212
405
|
if (hasSignificance) {
|
|
@@ -220,11 +413,18 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
220
413
|
}
|
|
221
414
|
}
|
|
222
415
|
async function runRescore(args, signal) {
|
|
223
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
416
|
+
const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
|
|
224
417
|
if (paths.length < 2) {
|
|
225
418
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
226
419
|
process.exit(1);
|
|
227
420
|
}
|
|
421
|
+
if (scorerNames?.length) {
|
|
422
|
+
console.error(
|
|
423
|
+
"Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
|
|
424
|
+
);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
|
|
228
428
|
const [resultsPath, evalFilePath] = paths;
|
|
229
429
|
const raw = JSON.parse(await readFileAsync(resultsPath, "utf-8"));
|
|
230
430
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
@@ -245,7 +445,12 @@ async function runRescore(args, signal) {
|
|
|
245
445
|
const rescored = [];
|
|
246
446
|
for (const resultData of results) {
|
|
247
447
|
if (signal.aborted) break;
|
|
248
|
-
rescored.push(
|
|
448
|
+
rescored.push(
|
|
449
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
450
|
+
signal,
|
|
451
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
|
|
452
|
+
})
|
|
453
|
+
);
|
|
249
454
|
}
|
|
250
455
|
for (const r of rescored) {
|
|
251
456
|
console.log("\n" + formatTable(r) + "\n");
|
|
@@ -295,14 +500,18 @@ function formatTable(result) {
|
|
|
295
500
|
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
296
501
|
for (const name of scorerNames) {
|
|
297
502
|
const s = result.summary.scorers[name];
|
|
298
|
-
const
|
|
299
|
-
|
|
503
|
+
const { scored, failed, skipped } = scorerSampleCounts(s, name, result.items);
|
|
504
|
+
const annotationParts = [];
|
|
505
|
+
if (failed > 0) annotationParts.push(`${scored}/${scored + failed} scored, ${failed} failed`);
|
|
506
|
+
if (skipped > 0) annotationParts.push(`${skipped} N/A`);
|
|
507
|
+
const annotation = annotationParts.length > 0 ? ` (${annotationParts.join(", ")})` : "";
|
|
508
|
+
if (scored === 0) {
|
|
300
509
|
lines.push(
|
|
301
|
-
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
|
|
510
|
+
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
|
|
302
511
|
);
|
|
303
512
|
} else {
|
|
304
513
|
lines.push(
|
|
305
|
-
` ${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)}`
|
|
514
|
+
` ${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}`
|
|
306
515
|
);
|
|
307
516
|
}
|
|
308
517
|
}
|
|
@@ -350,8 +559,10 @@ function formatMultiRunTable(summary) {
|
|
|
350
559
|
for (const name of scorerNames) {
|
|
351
560
|
const s = summary.scorers[name];
|
|
352
561
|
const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
|
|
562
|
+
const { scored, failed } = scorerSampleCounts(s, name);
|
|
563
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
353
564
|
lines.push(
|
|
354
|
-
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
|
|
565
|
+
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
|
|
355
566
|
);
|
|
356
567
|
}
|
|
357
568
|
if (summary.timing) {
|
|
@@ -365,6 +576,36 @@ function formatMultiRunTable(summary) {
|
|
|
365
576
|
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
366
577
|
return lines.join("\n");
|
|
367
578
|
}
|
|
579
|
+
function reportDegraded(result, label) {
|
|
580
|
+
const d = result.summary.degraded;
|
|
581
|
+
if (!d || d.length === 0) return false;
|
|
582
|
+
for (const x of d) {
|
|
583
|
+
const attempted = x.scored + x.failed;
|
|
584
|
+
const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
|
|
585
|
+
console.error(
|
|
586
|
+
`[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
function reportFullySkippedScorers(result, label) {
|
|
592
|
+
for (const [name, entry] of Object.entries(result.summary.scorers)) {
|
|
593
|
+
const { scored, failed, skipped } = scorerSampleCounts(entry, name, result.items);
|
|
594
|
+
if (skipped > 0 && scored + failed === 0) {
|
|
595
|
+
console.error(
|
|
596
|
+
`[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.`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function reportTotalWipeout(result, label) {
|
|
602
|
+
const { count, failures } = result.summary;
|
|
603
|
+
if (count === 0 || failures < count) return false;
|
|
604
|
+
console.error(
|
|
605
|
+
`[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
|
|
606
|
+
);
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
368
609
|
async function resolveRuntimeFromConfig(configPath) {
|
|
369
610
|
try {
|
|
370
611
|
const mod = await importModule(configPath, import.meta.url);
|
|
@@ -425,41 +666,17 @@ async function getRuntime(configArg, conditions) {
|
|
|
425
666
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
426
667
|
return new AxlRuntime();
|
|
427
668
|
}
|
|
428
|
-
function parseEvalArgs(args) {
|
|
429
|
-
let outputPath;
|
|
430
|
-
let configArg;
|
|
431
|
-
let conditions = [];
|
|
432
|
-
let runs = 1;
|
|
433
|
-
let captureTraces = false;
|
|
434
|
-
const paths = [];
|
|
435
|
-
for (let i = 0; i < args.length; i++) {
|
|
436
|
-
const arg = args[i];
|
|
437
|
-
if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
|
|
438
|
-
if (i + 1 >= args.length) {
|
|
439
|
-
console.error(`Error: ${arg} requires a value`);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
}
|
|
442
|
-
const value = args[++i];
|
|
443
|
-
if (arg === "--output") outputPath = value;
|
|
444
|
-
else if (arg === "--config") configArg = value;
|
|
445
|
-
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
446
|
-
else
|
|
447
|
-
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
448
|
-
} else if (arg === "--capture-traces") {
|
|
449
|
-
captureTraces = true;
|
|
450
|
-
} else if (arg.startsWith("--")) {
|
|
451
|
-
if (!KNOWN_FLAGS.has(arg)) {
|
|
452
|
-
console.error(`Unknown flag: ${arg}`);
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
paths.push(arg);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return { outputPath, configArg, conditions, runs, captureTraces, paths };
|
|
460
|
-
}
|
|
461
669
|
async function runEvalCommand(args, signal) {
|
|
462
|
-
const {
|
|
670
|
+
const {
|
|
671
|
+
outputPath,
|
|
672
|
+
configArg,
|
|
673
|
+
conditions,
|
|
674
|
+
runs,
|
|
675
|
+
captureTraces,
|
|
676
|
+
concurrency,
|
|
677
|
+
scorerNames,
|
|
678
|
+
paths
|
|
679
|
+
} = parseEvalArgs(args);
|
|
463
680
|
if (paths.length === 0) {
|
|
464
681
|
console.error("Error: No eval file path provided");
|
|
465
682
|
process.exit(1);
|
|
@@ -472,6 +689,10 @@ async function runEvalCommand(args, signal) {
|
|
|
472
689
|
console.error("Error: No eval files found");
|
|
473
690
|
process.exit(1);
|
|
474
691
|
}
|
|
692
|
+
if (scorerNames?.length && evalFiles.length > 1) {
|
|
693
|
+
console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
475
696
|
const runtime = await getRuntime(configArg, conditions);
|
|
476
697
|
const results = [];
|
|
477
698
|
let failedFiles = 0;
|
|
@@ -487,6 +708,22 @@ async function runEvalCommand(args, signal) {
|
|
|
487
708
|
failedFiles++;
|
|
488
709
|
continue;
|
|
489
710
|
}
|
|
711
|
+
evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
|
|
712
|
+
if (scorerNames?.length) {
|
|
713
|
+
const available = evalConfig.scorers.map((s) => s.name);
|
|
714
|
+
const unknown = scorerNames.filter((n) => !available.includes(n));
|
|
715
|
+
if (unknown.length) {
|
|
716
|
+
console.error(
|
|
717
|
+
`Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
|
|
718
|
+
);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
|
|
722
|
+
if (evalConfig.scorers.length === 0) {
|
|
723
|
+
console.error("Error: --scorers matched no scorers.");
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
490
727
|
const exported = pickExport(mod, "executeWorkflow");
|
|
491
728
|
let customExecute;
|
|
492
729
|
if (typeof exported === "function") {
|
|
@@ -543,6 +780,13 @@ async function runEvalCommand(args, signal) {
|
|
|
543
780
|
continue;
|
|
544
781
|
}
|
|
545
782
|
const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
|
|
783
|
+
const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
|
|
784
|
+
const stampFiltered = (result) => {
|
|
785
|
+
if (scorersRun) {
|
|
786
|
+
result.metadata.scorerFiltered = true;
|
|
787
|
+
result.metadata.scorersRun = scorersRun;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
546
790
|
if (runs > 1) {
|
|
547
791
|
const { randomUUID } = await import("crypto");
|
|
548
792
|
const runGroupId = randomUUID();
|
|
@@ -555,6 +799,7 @@ async function runEvalCommand(args, signal) {
|
|
|
555
799
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
556
800
|
result.metadata.runGroupId = runGroupId;
|
|
557
801
|
result.metadata.runIndex = r;
|
|
802
|
+
stampFiltered(result);
|
|
558
803
|
runResults.push(result);
|
|
559
804
|
} catch (err) {
|
|
560
805
|
runFailure = err instanceof Error ? err : new Error(String(err));
|
|
@@ -586,10 +831,23 @@ async function runEvalCommand(args, signal) {
|
|
|
586
831
|
const summary = aggregateRuns(runResults);
|
|
587
832
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
588
833
|
for (const r of runResults) results.push(r);
|
|
834
|
+
let anyFailing = false;
|
|
835
|
+
for (const r of runResults) {
|
|
836
|
+
const wipeout = reportTotalWipeout(r, filePath);
|
|
837
|
+
const degraded = reportDegraded(r, filePath);
|
|
838
|
+
reportFullySkippedScorers(r, filePath);
|
|
839
|
+
if (wipeout || degraded) anyFailing = true;
|
|
840
|
+
}
|
|
841
|
+
if (anyFailing && !partial) failedFiles++;
|
|
589
842
|
} else {
|
|
590
843
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
844
|
+
stampFiltered(result);
|
|
591
845
|
results.push(result);
|
|
592
846
|
console.log("\n" + formatTable(result) + "\n");
|
|
847
|
+
const wipeout = reportTotalWipeout(result, filePath);
|
|
848
|
+
const degraded = reportDegraded(result, filePath);
|
|
849
|
+
reportFullySkippedScorers(result, filePath);
|
|
850
|
+
if (wipeout || degraded) failedFiles++;
|
|
593
851
|
}
|
|
594
852
|
} catch (err) {
|
|
595
853
|
console.error(
|