@axlsdk/eval 0.17.9 → 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/index.d.cts CHANGED
@@ -5,6 +5,27 @@ type DatasetItem<TInput, TAnnotations = undefined> = {
5
5
  input: TInput;
6
6
  annotations?: TAnnotations;
7
7
  };
8
+ /**
9
+ * What to do when a dataset item's `annotations` contain keys that are absent
10
+ * from the `annotations` schema.
11
+ *
12
+ * Zod object schemas strip unknown keys by default, so an annotation field
13
+ * that isn't declared in the schema is silently dropped before it ever reaches
14
+ * a scorer. A scorer that reads such a field sees `undefined` and quietly
15
+ * degrades to a no-op — the eval still "passes", so the bug is invisible. This
16
+ * is especially easy to hit with file-based datasets, where the JSON is never
17
+ * type-checked.
18
+ *
19
+ * - `'warn'` (default) — log a single `console.warn` per dataset listing the
20
+ * dropped key paths. Non-breaking; surfaces the contract violation.
21
+ * - `'error'` — throw, listing the dropped key paths. The strict path for CI.
22
+ * - `'ignore'` — preserve the silent-strip behavior (skips detection entirely).
23
+ *
24
+ * Only `annotations` are checked, not `input`: stripped input fields flow to
25
+ * the workflow where they surface, whereas stripped annotations vanish into a
26
+ * silent scorer.
27
+ */
28
+ type ExtraKeyPolicy = 'warn' | 'error' | 'ignore';
8
29
  type DatasetConfig<TInput extends z.ZodType, TAnnotations extends z.ZodType = z.ZodType> = {
9
30
  name: string;
10
31
  schema: TInput;
@@ -13,11 +34,28 @@ type DatasetConfig<TInput extends z.ZodType, TAnnotations extends z.ZodType = z.
13
34
  file?: string;
14
35
  /** Base directory for resolving relative file paths. Defaults to cwd. */
15
36
  basePath?: string;
37
+ /**
38
+ * How to handle annotation keys not present in the `annotations` schema
39
+ * (Zod strips them by default). Defaults to `'warn'`. See {@link ExtraKeyPolicy}.
40
+ */
41
+ onExtraAnnotationKeys?: ExtraKeyPolicy;
16
42
  };
17
43
  type Dataset<TInput = unknown, TAnnotations = unknown> = {
18
44
  readonly name: string;
19
45
  readonly schema: z.ZodType;
20
46
  readonly annotationsSchema?: z.ZodType;
47
+ /**
48
+ * Annotation key paths the schema dropped during the most recent
49
+ * `getItems()` call (empty when none were dropped, or when
50
+ * `onExtraAnnotationKeys: 'ignore'`). Populated as a side effect of
51
+ * `getItems()` — read it synchronously right after awaiting, on the same
52
+ * instance; a later `getItems()` overwrites it (calling `getItems()`
53
+ * concurrently on one shared dataset is unsupported). The eval runner folds
54
+ * this into `EvalResult.metadata.droppedAnnotationKeys` so any consumer (the
55
+ * CLI logs it; UIs can render it) sees the same signal. Optional so
56
+ * hand-rolled `Dataset`-shaped objects (test harnesses) remain valid.
57
+ */
58
+ readonly droppedAnnotationKeys?: readonly string[];
21
59
  getItems(): Promise<DatasetItem<TInput, TAnnotations>[]>;
22
60
  };
23
61
  declare function dataset<TInput extends z.ZodType, TAnnotations extends z.ZodType>(config: DatasetConfig<TInput, TAnnotations>): Dataset<z.infer<TInput>, z.infer<TAnnotations>>;
@@ -86,10 +124,53 @@ type EvalConfig = {
86
124
  workflow: string;
87
125
  dataset: Dataset<unknown, unknown>;
88
126
  scorers: Scorer<unknown, unknown, unknown>[];
127
+ /** Item-level worker-pool size (how many dataset items run in parallel). Default 5. */
89
128
  concurrency?: number;
129
+ /**
130
+ * Per-item scorer fan-out — how many of an item's scorers run concurrently.
131
+ * Default 5 (matches `concurrency`; scorers are parallel-by-default). The
132
+ * worst-case number of concurrent scorer calls is `concurrency × scorerConcurrency`,
133
+ * so lower `concurrency` if a rate-limited judge model needs a tighter ceiling.
134
+ */
135
+ scorerConcurrency?: number;
90
136
  budget?: string;
137
+ /**
138
+ * Opt-in source-side trust gate. When set (0–1), `runEval` flags the run as
139
+ * `summary.degraded` if any scorer's failure rate exceeds tolerance, and the
140
+ * CLI exits non-zero. Enforcement is **type-aware**: deterministic scorers
141
+ * tolerate ZERO failures (a deterministic scorer that throws is a bug, not
142
+ * noise), while LLM scorers use this rate (a flaky judge / rate-limit storm).
143
+ *
144
+ * Failure rate = `failed / (scored + failed)` per scorer, where `scored` is
145
+ * the number of items that produced a valid numeric score and `failed` is the
146
+ * number whose scorer ran and threw / returned out-of-range. `0` means "any
147
+ * LLM failure degrades the run". Invalid values (non-finite, <0, >1) are
148
+ * ignored with a `console.warn`.
149
+ *
150
+ * Distinct from the gate-side `--max-scorer-error-rate` compare flag: this one
151
+ * fires at run/produce time (catch a thinned sample before it's saved), the
152
+ * other at consume/gate time (refuse to certify a thinned baseline/candidate).
153
+ */
154
+ failOnScorerErrorRate?: number;
91
155
  metadata?: Record<string, unknown>;
92
156
  };
157
+ /**
158
+ * One scorer whose failure rate tripped `EvalConfig.failOnScorerErrorRate`.
159
+ * Surfaced on `EvalSummary.degraded` so consumers (CLI exit code, Studio
160
+ * banner) can refuse to trust the mean without re-deriving the rate.
161
+ */
162
+ type DegradedScorer = {
163
+ scorer: string;
164
+ /** Observed failure rate `failed / (scored + failed)` (0 when nothing ran). */
165
+ rate: number;
166
+ /** The tolerance that was exceeded (0 for deterministic scorers). */
167
+ limit: number;
168
+ type: 'llm' | 'deterministic';
169
+ /** Items that produced a valid numeric score. */
170
+ scored: number;
171
+ /** Items whose scorer ran and failed (threw / out-of-range). */
172
+ failed: number;
173
+ };
93
174
  type EvalResult = {
94
175
  id: string;
95
176
  /**
@@ -108,6 +189,13 @@ type EvalResult = {
108
189
  * that need a single "primary" workflow should use `workflows[0]`.
109
190
  * - `scorerTypes: Record<string, 'llm' | 'deterministic'>`
110
191
  * - `runGroupId?: string`, `runIndex?: number` for multi-run groups
192
+ * - `droppedAnnotationKeys?: string[]` — annotation key paths the dataset
193
+ * schema stripped (present only when non-empty). Mirrors the dataset's
194
+ * `console.warn`; lets any consumer surface the same signal.
195
+ * - `scorerFiltered?: boolean` + `scorersRun?: string[]` — set when the CLI
196
+ * `--scorers` flag ran a subset of scorers. A guard so a filtered run isn't
197
+ * silently used as a full baseline (`compare` warns / refuses to gate on it).
198
+ * Distinct from the multi-run "partial" concept (see {@link EvalComparisonPartial}).
111
199
  */
112
200
  metadata: Record<string, unknown>;
113
201
  timestamp: string;
@@ -150,6 +238,22 @@ type EvalSummary = {
150
238
  max: number;
151
239
  p50: number;
152
240
  p95: number;
241
+ /**
242
+ * Number of items that produced a valid numeric score — the sample size
243
+ * the `mean` actually covers. Optional so pre-existing artifacts (and
244
+ * hand-rolled summaries) stay valid; absent ⇒ recompute from `items`.
245
+ * Named `scored` (NOT `n`) to avoid collision with
246
+ * `EvalComparison.scorers[].n` (paired-diff count).
247
+ */
248
+ scored?: number;
249
+ /**
250
+ * Number of items whose scorer RAN and failed (threw or returned
251
+ * out-of-range). Distinct from items skipped by cancellation, which land
252
+ * in neither bucket — so `scored + failed` is the honest "attempted"
253
+ * denominator and may be `<` the eligible item count. A non-zero `failed`
254
+ * means the `mean` was computed over a thinned sample.
255
+ */
256
+ failed?: number;
153
257
  }>;
154
258
  timing?: {
155
259
  mean: number;
@@ -158,6 +262,14 @@ type EvalSummary = {
158
262
  p50: number;
159
263
  p95: number;
160
264
  };
265
+ /**
266
+ * Populated by `runEval` ONLY when `EvalConfig.failOnScorerErrorRate` is set
267
+ * and one or more scorers exceeded tolerance. `runEval` never throws on this
268
+ * — it returns the (still-useful) result with this flag set, and the CLI /
269
+ * consumer decides whether to fail. Absent / empty ⇒ no degradation gate
270
+ * tripped (either not configured or all scorers within tolerance).
271
+ */
272
+ degraded?: DegradedScorer[];
161
273
  };
162
274
  /**
163
275
  * Sample-size context for one side of a comparison.
@@ -216,6 +328,18 @@ type EvalComparison = {
216
328
  pImprovement?: number;
217
329
  /** Number of paired item differences used for CI computation. */
218
330
  n?: number;
331
+ /**
332
+ * Per-side scorer success/failure counts over the SAME truncated pool the
333
+ * means and CI are computed from (not a separately-recomputed raw set), so
334
+ * a gate reads numbers consistent with what the table displays. Raw counts
335
+ * (consumer divides) keep this truncation-consistent. A non-zero
336
+ * `*Failed` means that side's mean rests on a thinned sample — the gate-side
337
+ * `--max-scorer-error-rate` flag refuses to certify when it's over tolerance.
338
+ */
339
+ baselineScored?: number;
340
+ baselineFailed?: number;
341
+ candidateScored?: number;
342
+ candidateFailed?: number;
219
343
  }>;
220
344
  timing?: {
221
345
  baselineMean: number;
@@ -297,6 +421,48 @@ declare function runEval(config: EvalConfig, executeWorkflow: (input: unknown, r
297
421
  }>, runtime: AxlRuntime, options?: RunEvalOptions): Promise<EvalResult>;
298
422
 
299
423
  declare function evalCompare(baseline: EvalResult | EvalResult[], candidate: EvalResult | EvalResult[], options?: EvalCompareOptions): EvalComparison;
424
+ /**
425
+ * Decide whether a comparison should be REFUSED under a gate-side scorer
426
+ * failure-rate limit (`axl-eval compare --max-scorer-error-rate`). Pure and
427
+ * testable — returns a human-readable refusal reason, or `null` to allow.
428
+ *
429
+ * Type-aware via the baseline's `scorerTypes` metadata: a scorer EXPLICITLY
430
+ * marked `'deterministic'` tolerates ZERO failures (a deterministic throw is a
431
+ * bug); everything else — including an unknown/absent type on a pre-`scorerTypes`
432
+ * artifact — uses the LLM `limit`. Defaulting unknown types to LLM (the
433
+ * permissive tier) is deliberate: classifying an old artifact's scorers as
434
+ * deterministic would hard-fail them on the first flake, contradicting the
435
+ * `--max-scorer-error-rate <n>` the user passed. Scorer NAMES are identical
436
+ * across sides (enforced by `evalCompare`), so reading types from the baseline
437
+ * is sufficient.
438
+ *
439
+ * Also refuses a scorer that produced ZERO scored items on a side — a mean over
440
+ * an empty sample can't be certified. Counts come from the same truncated pool
441
+ * `evalCompare` used for the means/CI (`baseline/candidateScored/Failed`).
442
+ */
443
+ declare function evaluateScorerErrorRateGate(comparison: EvalComparison, maxScorerErrorRate: number): string | null;
444
+
445
+ /** A scorer's failure-rate verdict against a tolerance — see {@link evaluateScorerTolerance}. */
446
+ type ScorerToleranceVerdict = {
447
+ /** `scored + failed` — the attempted denominator (excludes cancelled/never-run). */
448
+ attempted: number;
449
+ /** `failed / attempted`, or `0` when nothing was attempted. */
450
+ rate: number;
451
+ /** Over tolerance AND something was actually attempted. */
452
+ exceeds: boolean;
453
+ /** Nothing was attempted (`attempted === 0`) — no basis to certify a rate. */
454
+ zeroSample: boolean;
455
+ };
456
+ /**
457
+ * Single source of truth for the type-aware failure-rate decision, shared by the
458
+ * source-side gate (`runEval` → `summary.degraded`) and the gate-side compare
459
+ * gate (`evalCompare` consumers). Deterministic scorers tolerate ZERO failures
460
+ * (a deterministic throw is a bug, not noise); LLM scorers tolerate up to `limit`
461
+ * (`failed / attempted`). `exceeds` is never true when nothing was attempted —
462
+ * callers decide their own policy for `zeroSample` (the runner skips it; the
463
+ * compare gate refuses to certify a zero-sample scorer).
464
+ */
465
+ declare function evaluateScorerTolerance(scored: number, failed: number, type: 'llm' | 'deterministic', limit: number): ScorerToleranceVerdict;
300
466
 
301
467
  type BootstrapCIResult = {
302
468
  /** Lower bound of the confidence interval. */
@@ -323,7 +489,12 @@ declare function pairedBootstrapCI(differences: number[], options?: {
323
489
  }): BootstrapCIResult;
324
490
 
325
491
  type RescoreOptions = {
492
+ /** Item-level worker-pool size (how many saved items rescore in parallel). Default 5. */
326
493
  concurrency?: number;
494
+ /** Per-item scorer fan-out (how many scorers run concurrently within one item).
495
+ * Default 5 — matches `EvalConfig.scorerConcurrency`. Worst-case concurrent
496
+ * judge calls is `concurrency × scorerConcurrency`. */
497
+ scorerConcurrency?: number;
327
498
  /** Abort signal forwarded into ScorerContext so in-flight LLM scorer calls can
328
499
  * be cancelled mid-flight. Also checked between items to short-circuit
329
500
  * remaining work. Mirrors `RunEvalOptions.signal`. */
@@ -353,6 +524,10 @@ type MultiRunSummary = {
353
524
  std: number;
354
525
  min: number;
355
526
  max: number;
527
+ /** Total valid scores summed across all runs in the group. */
528
+ scored?: number;
529
+ /** Total ran-and-failed scorer invocations summed across all runs. */
530
+ failed?: number;
356
531
  }>;
357
532
  timing?: {
358
533
  mean: number;
@@ -364,4 +539,4 @@ type MultiRunSummary = {
364
539
  */
365
540
  declare function aggregateRuns(runs: EvalResult[]): MultiRunSummary;
366
541
 
367
- export { type BootstrapCIResult, type Dataset, type DatasetConfig, type DatasetItem, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalImprovement, type EvalItem, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type LlmScorerConfig, type MultiRunSummary, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerResult, aggregateRuns, dataset, defineEval, evalCompare, llmScorer, normalizeScorerResult, pairedBootstrapCI, rescore, runEval, scorer };
542
+ export { type BootstrapCIResult, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalImprovement, type EvalItem, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type LlmScorerConfig, type MultiRunSummary, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerResult, type ScorerToleranceVerdict, aggregateRuns, dataset, defineEval, evalCompare, evaluateScorerErrorRateGate, evaluateScorerTolerance, llmScorer, normalizeScorerResult, pairedBootstrapCI, rescore, runEval, scorer };
package/dist/index.d.ts CHANGED
@@ -5,6 +5,27 @@ type DatasetItem<TInput, TAnnotations = undefined> = {
5
5
  input: TInput;
6
6
  annotations?: TAnnotations;
7
7
  };
8
+ /**
9
+ * What to do when a dataset item's `annotations` contain keys that are absent
10
+ * from the `annotations` schema.
11
+ *
12
+ * Zod object schemas strip unknown keys by default, so an annotation field
13
+ * that isn't declared in the schema is silently dropped before it ever reaches
14
+ * a scorer. A scorer that reads such a field sees `undefined` and quietly
15
+ * degrades to a no-op — the eval still "passes", so the bug is invisible. This
16
+ * is especially easy to hit with file-based datasets, where the JSON is never
17
+ * type-checked.
18
+ *
19
+ * - `'warn'` (default) — log a single `console.warn` per dataset listing the
20
+ * dropped key paths. Non-breaking; surfaces the contract violation.
21
+ * - `'error'` — throw, listing the dropped key paths. The strict path for CI.
22
+ * - `'ignore'` — preserve the silent-strip behavior (skips detection entirely).
23
+ *
24
+ * Only `annotations` are checked, not `input`: stripped input fields flow to
25
+ * the workflow where they surface, whereas stripped annotations vanish into a
26
+ * silent scorer.
27
+ */
28
+ type ExtraKeyPolicy = 'warn' | 'error' | 'ignore';
8
29
  type DatasetConfig<TInput extends z.ZodType, TAnnotations extends z.ZodType = z.ZodType> = {
9
30
  name: string;
10
31
  schema: TInput;
@@ -13,11 +34,28 @@ type DatasetConfig<TInput extends z.ZodType, TAnnotations extends z.ZodType = z.
13
34
  file?: string;
14
35
  /** Base directory for resolving relative file paths. Defaults to cwd. */
15
36
  basePath?: string;
37
+ /**
38
+ * How to handle annotation keys not present in the `annotations` schema
39
+ * (Zod strips them by default). Defaults to `'warn'`. See {@link ExtraKeyPolicy}.
40
+ */
41
+ onExtraAnnotationKeys?: ExtraKeyPolicy;
16
42
  };
17
43
  type Dataset<TInput = unknown, TAnnotations = unknown> = {
18
44
  readonly name: string;
19
45
  readonly schema: z.ZodType;
20
46
  readonly annotationsSchema?: z.ZodType;
47
+ /**
48
+ * Annotation key paths the schema dropped during the most recent
49
+ * `getItems()` call (empty when none were dropped, or when
50
+ * `onExtraAnnotationKeys: 'ignore'`). Populated as a side effect of
51
+ * `getItems()` — read it synchronously right after awaiting, on the same
52
+ * instance; a later `getItems()` overwrites it (calling `getItems()`
53
+ * concurrently on one shared dataset is unsupported). The eval runner folds
54
+ * this into `EvalResult.metadata.droppedAnnotationKeys` so any consumer (the
55
+ * CLI logs it; UIs can render it) sees the same signal. Optional so
56
+ * hand-rolled `Dataset`-shaped objects (test harnesses) remain valid.
57
+ */
58
+ readonly droppedAnnotationKeys?: readonly string[];
21
59
  getItems(): Promise<DatasetItem<TInput, TAnnotations>[]>;
22
60
  };
23
61
  declare function dataset<TInput extends z.ZodType, TAnnotations extends z.ZodType>(config: DatasetConfig<TInput, TAnnotations>): Dataset<z.infer<TInput>, z.infer<TAnnotations>>;
@@ -86,10 +124,53 @@ type EvalConfig = {
86
124
  workflow: string;
87
125
  dataset: Dataset<unknown, unknown>;
88
126
  scorers: Scorer<unknown, unknown, unknown>[];
127
+ /** Item-level worker-pool size (how many dataset items run in parallel). Default 5. */
89
128
  concurrency?: number;
129
+ /**
130
+ * Per-item scorer fan-out — how many of an item's scorers run concurrently.
131
+ * Default 5 (matches `concurrency`; scorers are parallel-by-default). The
132
+ * worst-case number of concurrent scorer calls is `concurrency × scorerConcurrency`,
133
+ * so lower `concurrency` if a rate-limited judge model needs a tighter ceiling.
134
+ */
135
+ scorerConcurrency?: number;
90
136
  budget?: string;
137
+ /**
138
+ * Opt-in source-side trust gate. When set (0–1), `runEval` flags the run as
139
+ * `summary.degraded` if any scorer's failure rate exceeds tolerance, and the
140
+ * CLI exits non-zero. Enforcement is **type-aware**: deterministic scorers
141
+ * tolerate ZERO failures (a deterministic scorer that throws is a bug, not
142
+ * noise), while LLM scorers use this rate (a flaky judge / rate-limit storm).
143
+ *
144
+ * Failure rate = `failed / (scored + failed)` per scorer, where `scored` is
145
+ * the number of items that produced a valid numeric score and `failed` is the
146
+ * number whose scorer ran and threw / returned out-of-range. `0` means "any
147
+ * LLM failure degrades the run". Invalid values (non-finite, <0, >1) are
148
+ * ignored with a `console.warn`.
149
+ *
150
+ * Distinct from the gate-side `--max-scorer-error-rate` compare flag: this one
151
+ * fires at run/produce time (catch a thinned sample before it's saved), the
152
+ * other at consume/gate time (refuse to certify a thinned baseline/candidate).
153
+ */
154
+ failOnScorerErrorRate?: number;
91
155
  metadata?: Record<string, unknown>;
92
156
  };
157
+ /**
158
+ * One scorer whose failure rate tripped `EvalConfig.failOnScorerErrorRate`.
159
+ * Surfaced on `EvalSummary.degraded` so consumers (CLI exit code, Studio
160
+ * banner) can refuse to trust the mean without re-deriving the rate.
161
+ */
162
+ type DegradedScorer = {
163
+ scorer: string;
164
+ /** Observed failure rate `failed / (scored + failed)` (0 when nothing ran). */
165
+ rate: number;
166
+ /** The tolerance that was exceeded (0 for deterministic scorers). */
167
+ limit: number;
168
+ type: 'llm' | 'deterministic';
169
+ /** Items that produced a valid numeric score. */
170
+ scored: number;
171
+ /** Items whose scorer ran and failed (threw / out-of-range). */
172
+ failed: number;
173
+ };
93
174
  type EvalResult = {
94
175
  id: string;
95
176
  /**
@@ -108,6 +189,13 @@ type EvalResult = {
108
189
  * that need a single "primary" workflow should use `workflows[0]`.
109
190
  * - `scorerTypes: Record<string, 'llm' | 'deterministic'>`
110
191
  * - `runGroupId?: string`, `runIndex?: number` for multi-run groups
192
+ * - `droppedAnnotationKeys?: string[]` — annotation key paths the dataset
193
+ * schema stripped (present only when non-empty). Mirrors the dataset's
194
+ * `console.warn`; lets any consumer surface the same signal.
195
+ * - `scorerFiltered?: boolean` + `scorersRun?: string[]` — set when the CLI
196
+ * `--scorers` flag ran a subset of scorers. A guard so a filtered run isn't
197
+ * silently used as a full baseline (`compare` warns / refuses to gate on it).
198
+ * Distinct from the multi-run "partial" concept (see {@link EvalComparisonPartial}).
111
199
  */
112
200
  metadata: Record<string, unknown>;
113
201
  timestamp: string;
@@ -150,6 +238,22 @@ type EvalSummary = {
150
238
  max: number;
151
239
  p50: number;
152
240
  p95: number;
241
+ /**
242
+ * Number of items that produced a valid numeric score — the sample size
243
+ * the `mean` actually covers. Optional so pre-existing artifacts (and
244
+ * hand-rolled summaries) stay valid; absent ⇒ recompute from `items`.
245
+ * Named `scored` (NOT `n`) to avoid collision with
246
+ * `EvalComparison.scorers[].n` (paired-diff count).
247
+ */
248
+ scored?: number;
249
+ /**
250
+ * Number of items whose scorer RAN and failed (threw or returned
251
+ * out-of-range). Distinct from items skipped by cancellation, which land
252
+ * in neither bucket — so `scored + failed` is the honest "attempted"
253
+ * denominator and may be `<` the eligible item count. A non-zero `failed`
254
+ * means the `mean` was computed over a thinned sample.
255
+ */
256
+ failed?: number;
153
257
  }>;
154
258
  timing?: {
155
259
  mean: number;
@@ -158,6 +262,14 @@ type EvalSummary = {
158
262
  p50: number;
159
263
  p95: number;
160
264
  };
265
+ /**
266
+ * Populated by `runEval` ONLY when `EvalConfig.failOnScorerErrorRate` is set
267
+ * and one or more scorers exceeded tolerance. `runEval` never throws on this
268
+ * — it returns the (still-useful) result with this flag set, and the CLI /
269
+ * consumer decides whether to fail. Absent / empty ⇒ no degradation gate
270
+ * tripped (either not configured or all scorers within tolerance).
271
+ */
272
+ degraded?: DegradedScorer[];
161
273
  };
162
274
  /**
163
275
  * Sample-size context for one side of a comparison.
@@ -216,6 +328,18 @@ type EvalComparison = {
216
328
  pImprovement?: number;
217
329
  /** Number of paired item differences used for CI computation. */
218
330
  n?: number;
331
+ /**
332
+ * Per-side scorer success/failure counts over the SAME truncated pool the
333
+ * means and CI are computed from (not a separately-recomputed raw set), so
334
+ * a gate reads numbers consistent with what the table displays. Raw counts
335
+ * (consumer divides) keep this truncation-consistent. A non-zero
336
+ * `*Failed` means that side's mean rests on a thinned sample — the gate-side
337
+ * `--max-scorer-error-rate` flag refuses to certify when it's over tolerance.
338
+ */
339
+ baselineScored?: number;
340
+ baselineFailed?: number;
341
+ candidateScored?: number;
342
+ candidateFailed?: number;
219
343
  }>;
220
344
  timing?: {
221
345
  baselineMean: number;
@@ -297,6 +421,48 @@ declare function runEval(config: EvalConfig, executeWorkflow: (input: unknown, r
297
421
  }>, runtime: AxlRuntime, options?: RunEvalOptions): Promise<EvalResult>;
298
422
 
299
423
  declare function evalCompare(baseline: EvalResult | EvalResult[], candidate: EvalResult | EvalResult[], options?: EvalCompareOptions): EvalComparison;
424
+ /**
425
+ * Decide whether a comparison should be REFUSED under a gate-side scorer
426
+ * failure-rate limit (`axl-eval compare --max-scorer-error-rate`). Pure and
427
+ * testable — returns a human-readable refusal reason, or `null` to allow.
428
+ *
429
+ * Type-aware via the baseline's `scorerTypes` metadata: a scorer EXPLICITLY
430
+ * marked `'deterministic'` tolerates ZERO failures (a deterministic throw is a
431
+ * bug); everything else — including an unknown/absent type on a pre-`scorerTypes`
432
+ * artifact — uses the LLM `limit`. Defaulting unknown types to LLM (the
433
+ * permissive tier) is deliberate: classifying an old artifact's scorers as
434
+ * deterministic would hard-fail them on the first flake, contradicting the
435
+ * `--max-scorer-error-rate <n>` the user passed. Scorer NAMES are identical
436
+ * across sides (enforced by `evalCompare`), so reading types from the baseline
437
+ * is sufficient.
438
+ *
439
+ * Also refuses a scorer that produced ZERO scored items on a side — a mean over
440
+ * an empty sample can't be certified. Counts come from the same truncated pool
441
+ * `evalCompare` used for the means/CI (`baseline/candidateScored/Failed`).
442
+ */
443
+ declare function evaluateScorerErrorRateGate(comparison: EvalComparison, maxScorerErrorRate: number): string | null;
444
+
445
+ /** A scorer's failure-rate verdict against a tolerance — see {@link evaluateScorerTolerance}. */
446
+ type ScorerToleranceVerdict = {
447
+ /** `scored + failed` — the attempted denominator (excludes cancelled/never-run). */
448
+ attempted: number;
449
+ /** `failed / attempted`, or `0` when nothing was attempted. */
450
+ rate: number;
451
+ /** Over tolerance AND something was actually attempted. */
452
+ exceeds: boolean;
453
+ /** Nothing was attempted (`attempted === 0`) — no basis to certify a rate. */
454
+ zeroSample: boolean;
455
+ };
456
+ /**
457
+ * Single source of truth for the type-aware failure-rate decision, shared by the
458
+ * source-side gate (`runEval` → `summary.degraded`) and the gate-side compare
459
+ * gate (`evalCompare` consumers). Deterministic scorers tolerate ZERO failures
460
+ * (a deterministic throw is a bug, not noise); LLM scorers tolerate up to `limit`
461
+ * (`failed / attempted`). `exceeds` is never true when nothing was attempted —
462
+ * callers decide their own policy for `zeroSample` (the runner skips it; the
463
+ * compare gate refuses to certify a zero-sample scorer).
464
+ */
465
+ declare function evaluateScorerTolerance(scored: number, failed: number, type: 'llm' | 'deterministic', limit: number): ScorerToleranceVerdict;
300
466
 
301
467
  type BootstrapCIResult = {
302
468
  /** Lower bound of the confidence interval. */
@@ -323,7 +489,12 @@ declare function pairedBootstrapCI(differences: number[], options?: {
323
489
  }): BootstrapCIResult;
324
490
 
325
491
  type RescoreOptions = {
492
+ /** Item-level worker-pool size (how many saved items rescore in parallel). Default 5. */
326
493
  concurrency?: number;
494
+ /** Per-item scorer fan-out (how many scorers run concurrently within one item).
495
+ * Default 5 — matches `EvalConfig.scorerConcurrency`. Worst-case concurrent
496
+ * judge calls is `concurrency × scorerConcurrency`. */
497
+ scorerConcurrency?: number;
327
498
  /** Abort signal forwarded into ScorerContext so in-flight LLM scorer calls can
328
499
  * be cancelled mid-flight. Also checked between items to short-circuit
329
500
  * remaining work. Mirrors `RunEvalOptions.signal`. */
@@ -353,6 +524,10 @@ type MultiRunSummary = {
353
524
  std: number;
354
525
  min: number;
355
526
  max: number;
527
+ /** Total valid scores summed across all runs in the group. */
528
+ scored?: number;
529
+ /** Total ran-and-failed scorer invocations summed across all runs. */
530
+ failed?: number;
356
531
  }>;
357
532
  timing?: {
358
533
  mean: number;
@@ -364,4 +539,4 @@ type MultiRunSummary = {
364
539
  */
365
540
  declare function aggregateRuns(runs: EvalResult[]): MultiRunSummary;
366
541
 
367
- export { type BootstrapCIResult, type Dataset, type DatasetConfig, type DatasetItem, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalImprovement, type EvalItem, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type LlmScorerConfig, type MultiRunSummary, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerResult, aggregateRuns, dataset, defineEval, evalCompare, llmScorer, normalizeScorerResult, pairedBootstrapCI, rescore, runEval, scorer };
542
+ export { type BootstrapCIResult, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalImprovement, type EvalItem, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type LlmScorerConfig, type MultiRunSummary, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerResult, type ScorerToleranceVerdict, aggregateRuns, dataset, defineEval, evalCompare, evaluateScorerErrorRateGate, evaluateScorerTolerance, llmScorer, normalizeScorerResult, pairedBootstrapCI, rescore, runEval, scorer };
package/dist/index.js CHANGED
@@ -1,21 +1,84 @@
1
1
  import {
2
2
  aggregateRuns,
3
+ attachScorerErrorCost,
3
4
  evalCompare,
5
+ evaluateScorerErrorRateGate,
6
+ evaluateScorerTolerance,
4
7
  normalizeScorerResult,
5
8
  pairedBootstrapCI,
6
9
  rescore,
7
10
  runEval,
8
11
  scorer
9
- } from "./chunk-NETD4AUB.js";
12
+ } from "./chunk-BWBX5YVQ.js";
10
13
 
11
14
  // src/dataset.ts
12
15
  import { readFile } from "fs/promises";
13
16
  import * as path from "path";
17
+ function isPlainObject(value) {
18
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
19
+ const proto = Object.getPrototypeOf(value);
20
+ return proto === Object.prototype || proto === null;
21
+ }
22
+ function isObjectSchema(schema) {
23
+ const shape = schema.shape;
24
+ return typeof shape === "object" && shape !== null;
25
+ }
26
+ function collectDroppedKeys(raw, parsed, prefix = "") {
27
+ if (Array.isArray(raw) && Array.isArray(parsed)) {
28
+ const dropped = [];
29
+ const n = Math.min(raw.length, parsed.length);
30
+ for (let i = 0; i < n; i++) {
31
+ dropped.push(...collectDroppedKeys(raw[i], parsed[i], `${prefix}[${i}]`));
32
+ }
33
+ return dropped;
34
+ }
35
+ if (isPlainObject(raw) && isPlainObject(parsed)) {
36
+ const dropped = [];
37
+ for (const key of Object.keys(raw)) {
38
+ const path2 = prefix ? `${prefix}.${key}` : key;
39
+ if (!(key in parsed)) {
40
+ dropped.push(path2);
41
+ } else {
42
+ dropped.push(...collectDroppedKeys(raw[key], parsed[key], path2));
43
+ }
44
+ }
45
+ return dropped;
46
+ }
47
+ return [];
48
+ }
14
49
  function dataset(config) {
50
+ const policy = config.onExtraAnnotationKeys ?? "warn";
51
+ function parseItem(item, droppedAcc) {
52
+ const input = config.schema.parse(item.input);
53
+ if (item.annotations == null || !config.annotations) {
54
+ return { input, annotations: item.annotations };
55
+ }
56
+ const annotations = config.annotations.parse(item.annotations);
57
+ if (policy !== "ignore" && isObjectSchema(config.annotations)) {
58
+ for (const p of collectDroppedKeys(item.annotations, annotations)) droppedAcc.add(p);
59
+ }
60
+ return { input, annotations };
61
+ }
62
+ function reportDropped(dropped) {
63
+ if (dropped.size === 0) return;
64
+ const keys = [...dropped].join(", ");
65
+ if (policy === "error") {
66
+ throw new Error(
67
+ `Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will never reach scorers. Add them to the schema, or set onExtraAnnotationKeys: 'ignore' to allow stripping.`
68
+ );
69
+ }
70
+ console.warn(
71
+ `[axl-eval] Dataset "${config.name}": annotation key(s) dropped by schema: ${keys}. These keys are not declared in the annotations schema, so they were stripped and will not reach scorers (a common cause of no-op scorers). Add them to the schema, set onExtraAnnotationKeys: 'error' to fail, or 'ignore' to silence this warning.`
72
+ );
73
+ }
74
+ let lastDroppedAnnotationKeys = [];
15
75
  return {
16
76
  name: config.name,
17
77
  schema: config.schema,
18
78
  annotationsSchema: config.annotations,
79
+ get droppedAnnotationKeys() {
80
+ return [...lastDroppedAnnotationKeys];
81
+ },
19
82
  async getItems() {
20
83
  if (config.items && config.file) {
21
84
  throw new Error('Dataset config error: "items" and "file" are mutually exclusive');
@@ -23,19 +86,19 @@ function dataset(config) {
23
86
  if (!config.items && !config.file) {
24
87
  throw new Error('Dataset config error: either "items" or "file" must be provided');
25
88
  }
89
+ let rawItems;
26
90
  if (config.items) {
27
- return config.items.map((item) => ({
28
- input: config.schema.parse(item.input),
29
- annotations: item.annotations && config.annotations ? config.annotations.parse(item.annotations) : item.annotations
30
- }));
91
+ rawItems = config.items;
92
+ } else {
93
+ const resolvedPath = path.resolve(config.basePath ?? process.cwd(), config.file);
94
+ const content = await readFile(resolvedPath, "utf-8");
95
+ rawItems = JSON.parse(content);
31
96
  }
32
- const resolvedPath = path.resolve(config.basePath ?? process.cwd(), config.file);
33
- const content = await readFile(resolvedPath, "utf-8");
34
- const items = JSON.parse(content);
35
- return items.map((item) => ({
36
- input: config.schema.parse(item.input),
37
- annotations: item.annotations && config.annotations ? config.annotations.parse(item.annotations) : item.annotations
38
- }));
97
+ const dropped = /* @__PURE__ */ new Set();
98
+ const parsed = rawItems.map((item) => parseItem(item, dropped));
99
+ lastDroppedAnnotationKeys = [...dropped];
100
+ reportDropped(dropped);
101
+ return parsed;
39
102
  }
40
103
  };
41
104
  }
@@ -96,16 +159,14 @@ ${JSON.stringify(annotations, null, 2)}` : "",
96
159
  const parsed = JSON.parse(extractJson(response.content));
97
160
  validated = schema.parse(parsed);
98
161
  } catch (err) {
99
- if (err && typeof err === "object") {
100
- err.cost = responseCost;
101
- }
162
+ attachScorerErrorCost(err, responseCost);
102
163
  if (err && typeof err === "object" && "issues" in err && Array.isArray(err.issues)) {
103
164
  const issues = err.issues;
104
165
  const messages = issues.map((i) => `${i.path.length ? `${i.path.join(".")}: ` : ""}${i.message}`).join("; ");
105
166
  const error = new Error(
106
167
  `LLM scorer "${config.name}" returned an invalid response: ${messages}`
107
168
  );
108
- error.cost = responseCost;
169
+ attachScorerErrorCost(error, responseCost);
109
170
  throw error;
110
171
  }
111
172
  throw err;
@@ -129,6 +190,8 @@ export {
129
190
  dataset,
130
191
  defineEval,
131
192
  evalCompare,
193
+ evaluateScorerErrorRateGate,
194
+ evaluateScorerTolerance,
132
195
  llmScorer,
133
196
  normalizeScorerResult,
134
197
  pairedBootstrapCI,