@rulvar/evals 1.82.0 → 1.84.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.ts CHANGED
@@ -421,7 +421,8 @@ interface RunBenchmarkOptions {
421
421
  /**
422
422
  * Aggregate debit-only envelope: every target and judge run
423
423
  * authorizes its ceiling here BEFORE starting, exactly like the eval
424
- * runners. A target-run refusal throws SweepBudgetError; a judge-run
424
+ * runners. A target-run refusal ends the series monotonically
425
+ * (report.refusal) with every completed repeat preserved; a judge-run
425
426
  * refusal rejects that run from scoring as 'judge:refused'.
426
427
  */
427
428
  envelope?: SpendEnvelope;
@@ -520,13 +521,26 @@ interface BenchmarkReport {
520
521
  /** Every target and judge run, scored or rejected (honest spend). */
521
522
  totalCostUsd: number;
522
523
  judgeCostUsd: number;
524
+ /**
525
+ * Present when the aggregate envelope refused a TARGET run before it
526
+ * started (cycle 81): the series ends there and every completed
527
+ * repeat stays on the report, mirroring the eval suite's monotone
528
+ * refusal instead of a throw destroying the paid evidence. Judge
529
+ * refusals never appear here; they reject their own run as
530
+ * 'judge:refused'.
531
+ */
532
+ refusal?: {
533
+ atOrdinal: number;
534
+ detail: string;
535
+ };
523
536
  fingerprint: BenchmarkFingerprint;
524
537
  }
525
538
  /**
526
539
  * Runs the spec's repeats sequentially and reports the verified series.
527
540
  * Throws only for spec defects (invalid repeats, a throwing grader or
528
- * extractor) and for a target-run envelope refusal; everything a run
529
- * does wrong lands in its record instead.
541
+ * extractor); everything a run does wrong lands in its record, and a
542
+ * target-run envelope refusal ends the series monotonically with the
543
+ * completed repeats preserved (report.refusal).
530
544
  */
531
545
  declare function runBenchmark(engine: Engine, spec: BenchmarkSpec, options?: RunBenchmarkOptions): Promise<BenchmarkReport>;
532
546
  //#endregion
@@ -622,6 +636,15 @@ interface SweepCellReport {
622
636
  */
623
637
  exhaustedRuns?: number;
624
638
  /**
639
+ * Count of case results whose TARGET run settled neither ok nor
640
+ * exhausted ('error', 'cancelled', 'suspended'): measurement
641
+ * artifacts, not model quality. Exactly like exhaustedRuns, any such
642
+ * run suppresses the cell's claim: a passRate deflated by a provider
643
+ * failure or a host cancellation must not become a committed model
644
+ * weakness (cycle 81).
645
+ */
646
+ nonOkRuns?: number;
647
+ /**
625
648
  * Count of result rows whose grading stopped on a judge budget event
626
649
  * (per-run judge ceiling or envelope refusal of a judge run). The
627
650
  * paid target evidence stays on those rows; the cell emits no claim.
@@ -707,6 +730,15 @@ interface CheckpointCell {
707
730
  recommended: boolean;
708
731
  baseline: CheckpointArm;
709
732
  treatment: CheckpointArm;
733
+ /**
734
+ * Either arm carried a measurement artifact (an envelope refusal, an
735
+ * incomplete row, or a target that settled non-ok): the arms are not
736
+ * comparable, the cell can never pass, and criterion 1 fails
737
+ * (cycle 81). Without this, an envelope drained by the baseline left
738
+ * an EMPTY refused treatment arm (n 0, cost 0) that mechanically beat
739
+ * any baseline under the cheaper-at-equal-quality branch.
740
+ */
741
+ contaminated?: true;
710
742
  passed: boolean;
711
743
  }
712
744
  interface CriterionOneReport {
@@ -716,11 +748,15 @@ interface CriterionOneReport {
716
748
  pooledBaseline: CheckpointArm;
717
749
  pooledTreatment: CheckpointArm;
718
750
  pooledHolds: boolean;
751
+ /** Cells with a contaminated arm; present when nonzero (cycle 81). */
752
+ contaminatedCells?: number;
719
753
  passed: boolean;
720
754
  }
721
755
  interface CriterionTwoReport {
722
756
  baseline: CheckpointArm;
723
757
  informed: CheckpointArm;
758
+ /** Either arm carried a measurement artifact; the criterion cannot pass. */
759
+ contaminated?: true;
724
760
  passed: boolean;
725
761
  }
726
762
  interface CheckpointReport {
package/dist/index.js CHANGED
@@ -760,8 +760,9 @@ function workflowWarningsOf(events) {
760
760
  /**
761
761
  * Runs the spec's repeats sequentially and reports the verified series.
762
762
  * Throws only for spec defects (invalid repeats, a throwing grader or
763
- * extractor) and for a target-run envelope refusal; everything a run
764
- * does wrong lands in its record instead.
763
+ * extractor); everything a run does wrong lands in its record, and a
764
+ * target-run envelope refusal ends the series monotonically with the
765
+ * completed repeats preserved (report.refusal).
765
766
  */
766
767
  async function runBenchmark(engine, spec, options = {}) {
767
768
  if (!Number.isInteger(spec.repeats) || spec.repeats < 1) throw new TypeError(`BenchmarkSpec.repeats must be a positive integer, got ${String(spec.repeats)}`);
@@ -769,8 +770,18 @@ async function runBenchmark(engine, spec, options = {}) {
769
770
  let startedAt;
770
771
  let totalCostUsd = 0;
771
772
  let totalJudgeCostUsd = 0;
773
+ let refusal;
772
774
  for (let ordinal = 1; ordinal <= spec.repeats; ordinal += 1) {
773
- options.envelope?.authorize(options.budgetUsd, `benchmark '${spec.name}' run ${String(ordinal)}`);
775
+ try {
776
+ options.envelope?.authorize(options.budgetUsd, `benchmark '${spec.name}' run ${String(ordinal)}`);
777
+ } catch (error) {
778
+ if (!(error instanceof SweepBudgetError)) throw error;
779
+ refusal = {
780
+ atOrdinal: ordinal,
781
+ detail: error.message
782
+ };
783
+ break;
784
+ }
774
785
  const live = await collectRun(engine.run(spec.workflow, spec.args, {
775
786
  name: `benchmark:${spec.name}:${String(ordinal)}`,
776
787
  ...options.budgetUsd === void 0 ? {} : { budgetUsd: options.budgetUsd }
@@ -886,6 +897,7 @@ async function runBenchmark(engine, spec, options = {}) {
886
897
  metrics: metricSeries,
887
898
  totalCostUsd,
888
899
  judgeCostUsd: totalJudgeCostUsd,
900
+ ...refusal === void 0 ? {} : { refusal },
889
901
  fingerprint: {
890
902
  node: process.version,
891
903
  platform: process.platform,
@@ -958,6 +970,16 @@ function armOf(suite) {
958
970
  n: suite.results.length
959
971
  };
960
972
  }
973
+ /**
974
+ * A measurement artifact in an arm invalidates the A/B comparison: an
975
+ * envelope refusal truncated the arm, a judge budget event forced rows
976
+ * to fail, or a target settled non-ok (provider failure, ceiling, host
977
+ * cancellation). Grader failures on ok targets are the HONEST failure
978
+ * signal and never contaminate.
979
+ */
980
+ function contaminatedArm(suite) {
981
+ return suite.refusal !== void 0 || suite.results.some((result) => result.status !== "ok" || result.incomplete !== void 0);
982
+ }
961
983
  function pool(arms) {
962
984
  const n = arms.reduce((sum, arm) => sum + arm.n, 0);
963
985
  const passed = arms.reduce((sum, arm) => sum + arm.passRate * arm.n, 0);
@@ -987,9 +1009,15 @@ async function runValueCheckpoint(checkpointPool, options) {
987
1009
  const treatmentTier = recommendation?.recommendedTier ?? ladder.startTier;
988
1010
  const baseMember = ladder.rungs[ladder.startTier];
989
1011
  const treatMember = ladder.rungs[treatmentTier];
990
- if (baseMember === void 0 || treatMember === void 0) throw new Error(`checkpoint: ladder '${ladder.name}' lacks rung ${String(treatmentTier)}`);
991
- const baseline = armOf(await runEvalSuite(await options.engineFor(baseMember), cases, options.suite ?? {}));
992
- const treatment = treatmentTier === ladder.startTier ? baseline : armOf(await runEvalSuite(await options.engineFor(treatMember), cases, options.suite ?? {}));
1012
+ if (baseMember === void 0 || treatMember === void 0) {
1013
+ const missing = baseMember === void 0 ? ladder.startTier : treatmentTier;
1014
+ throw new Error(`checkpoint: ladder '${ladder.name}' lacks rung ${String(missing)}`);
1015
+ }
1016
+ const baselineSuite = await runEvalSuite(await options.engineFor(baseMember), cases, options.suite ?? {});
1017
+ const treatmentSuite = treatmentTier === ladder.startTier ? baselineSuite : await runEvalSuite(await options.engineFor(treatMember), cases, options.suite ?? {});
1018
+ const baseline = armOf(baselineSuite);
1019
+ const treatment = armOf(treatmentSuite);
1020
+ const contaminated = contaminatedArm(baselineSuite) || contaminatedArm(treatmentSuite);
993
1021
  cells.push({
994
1022
  ladder: ladder.name,
995
1023
  taskClass,
@@ -998,7 +1026,8 @@ async function runValueCheckpoint(checkpointPool, options) {
998
1026
  recommended: recommendation !== void 0,
999
1027
  baseline,
1000
1028
  treatment,
1001
- passed: rungRuleHolds(baseline, treatment)
1029
+ ...contaminated ? { contaminated: true } : {},
1030
+ passed: !contaminated && rungRuleHolds(baseline, treatment)
1002
1031
  });
1003
1032
  }
1004
1033
  const recommendedCells = cells.filter((cell) => cell.recommended);
@@ -1007,6 +1036,7 @@ async function runValueCheckpoint(checkpointPool, options) {
1007
1036
  const pooledBaseline = pool(cells.map((cell) => cell.baseline));
1008
1037
  const pooledTreatment = pool(cells.map((cell) => cell.treatment));
1009
1038
  const pooledHolds = rungRuleHolds(pooledBaseline, pooledTreatment);
1039
+ const contaminatedCells = cells.filter((cell) => cell.contaminated === true).length;
1010
1040
  const criterion1 = {
1011
1041
  cells,
1012
1042
  cellsPassed,
@@ -1014,18 +1044,23 @@ async function runValueCheckpoint(checkpointPool, options) {
1014
1044
  pooledBaseline,
1015
1045
  pooledTreatment,
1016
1046
  pooledHolds,
1017
- passed: majorityHolds && pooledHolds
1047
+ ...contaminatedCells === 0 ? {} : { contaminatedCells },
1048
+ passed: majorityHolds && pooledHolds && contaminatedCells === 0
1018
1049
  };
1019
1050
  let criterion2;
1020
1051
  if (options.orchestrateEngineFor !== void 0 && options.orchestratedCases !== void 0) {
1021
1052
  const cases = options.orchestratedCases.map((entry) => entry.case);
1022
1053
  const orchestratedSuite = options.orchestratedSuite ?? options.suite ?? {};
1023
- const baseline = armOf(await runEvalSuite(await options.orchestrateEngineFor(false), cases, orchestratedSuite));
1024
- const informed = armOf(await runEvalSuite(await options.orchestrateEngineFor(true), cases, orchestratedSuite));
1054
+ const baselineSuite = await runEvalSuite(await options.orchestrateEngineFor(false), cases, orchestratedSuite);
1055
+ const informedSuite = await runEvalSuite(await options.orchestrateEngineFor(true), cases, orchestratedSuite);
1056
+ const baseline = armOf(baselineSuite);
1057
+ const informed = armOf(informedSuite);
1058
+ const contaminated = contaminatedArm(baselineSuite) || contaminatedArm(informedSuite);
1025
1059
  criterion2 = {
1026
1060
  baseline,
1027
1061
  informed,
1028
- passed: informed.n > 0 && informed.passRate > 0 && agentTypeRuleHolds(baseline, informed)
1062
+ ...contaminated ? { contaminated: true } : {},
1063
+ passed: !contaminated && informed.n > 0 && informed.passRate > 0 && agentTypeRuleHolds(baseline, informed)
1029
1064
  };
1030
1065
  }
1031
1066
  return {
@@ -1042,9 +1077,9 @@ function renderCheckpointReport(report) {
1042
1077
  const lines = [
1043
1078
  `Measured-value checkpoint (OQ-09) at ${report.observedAt}: ` + (report.passed ? "PASSED" : "FAILED"),
1044
1079
  "",
1045
- `Criterion 1 (rung selection): ${report.criterion1.passed ? "holds" : "fails"} (${String(report.criterion1.cellsPassed)}/${String(report.criterion1.cells.length)} cells, pooled ${report.criterion1.pooledHolds ? "holds" : "fails"})`
1080
+ `Criterion 1 (rung selection): ${report.criterion1.passed ? "holds" : "fails"} (${String(report.criterion1.cellsPassed)}/${String(report.criterion1.cells.filter((cell) => cell.recommended).length)} recommended cells, pooled ${report.criterion1.pooledHolds ? "holds" : "fails"})`
1046
1081
  ];
1047
- for (const cell of report.criterion1.cells) lines.push(`* ${cell.ladder} :: ${cell.taskClass}: baseline tier ${String(cell.defaultTier)} ${percent(cell.baseline.passRate)} at ${usd(cell.baseline.totalCostUsd)}; treatment tier ${String(cell.treatmentTier)}${cell.recommended ? "" : " (no recommendation)"} ${percent(cell.treatment.passRate)} at ${usd(cell.treatment.totalCostUsd)}; ${cell.passed ? "pass" : "fail"} (n=${String(cell.baseline.n)})`);
1082
+ for (const cell of report.criterion1.cells) lines.push(`* ${cell.ladder} :: ${cell.taskClass}: baseline tier ${String(cell.defaultTier)} ${percent(cell.baseline.passRate)} at ${usd(cell.baseline.totalCostUsd)}; treatment tier ${String(cell.treatmentTier)}${cell.recommended ? "" : " (no recommendation)"} ${percent(cell.treatment.passRate)} at ${usd(cell.treatment.totalCostUsd)}; ${cell.passed ? "pass" : "fail"}${cell.contaminated === true ? " (contaminated)" : ""} (n=${String(cell.baseline.n)})`);
1048
1083
  if (report.criterion2 !== void 0) {
1049
1084
  const c2 = report.criterion2;
1050
1085
  lines.push("", `Criterion 2 (agentType selection): ${c2.passed ? "holds" : "fails"} (baseline ${percent(c2.baseline.passRate)} at ${usd(c2.baseline.totalCostUsd)}; card-informed ${percent(c2.informed.passRate)} at ${usd(c2.informed.totalCostUsd)}; n=${String(c2.baseline.n)})`);
@@ -1108,6 +1143,7 @@ async function runSweepMatrix(pool, options) {
1108
1143
  ...options.envelope === void 0 ? {} : { envelope: options.envelope }
1109
1144
  });
1110
1145
  const exhaustedRuns = suite.results.filter((result) => result.status === "exhausted").length;
1146
+ const nonOkRuns = suite.results.filter((result) => result.status !== "ok" && result.status !== "exhausted").length;
1111
1147
  const judgeIncompleteRuns = suite.results.filter((result) => result.incomplete !== void 0).length;
1112
1148
  const incompleteReason = suite.refusal !== void 0 ? "envelope-exhausted" : suite.results.find((result) => result.incomplete !== void 0)?.incomplete?.reason;
1113
1149
  const cell = {
@@ -1120,6 +1156,7 @@ async function runSweepMatrix(pool, options) {
1120
1156
  totalCostUsd: suite.totalCostUsd,
1121
1157
  caseNames: suite.results.map((result) => result.name),
1122
1158
  ...exhaustedRuns === 0 ? {} : { exhaustedRuns },
1159
+ ...nonOkRuns === 0 ? {} : { nonOkRuns },
1123
1160
  ...judgeIncompleteRuns === 0 ? {} : { judgeIncompleteRuns },
1124
1161
  ...suite.refusal === void 0 ? {} : {
1125
1162
  envelopeExhausted: true,
@@ -1129,7 +1166,7 @@ async function runSweepMatrix(pool, options) {
1129
1166
  };
1130
1167
  cells.push(cell);
1131
1168
  const polarity = cell.passRate >= thresholds.strength ? "strength" : cell.passRate <= thresholds.weakness ? "weakness" : void 0;
1132
- const complete = cell.n === cell.plannedN && exhaustedRuns === 0 && judgeIncompleteRuns === 0 && suite.refusal === void 0;
1169
+ const complete = cell.n === cell.plannedN && exhaustedRuns === 0 && nonOkRuns === 0 && judgeIncompleteRuns === 0 && suite.refusal === void 0;
1133
1170
  if (polarity !== void 0 && cell.n > 0 && complete) {
1134
1171
  const epoch = options.modelEpochFor?.(member);
1135
1172
  claims.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/evals",
3
- "version": "1.82.0",
3
+ "version": "1.84.0",
4
4
  "description": "Rulvar evals: eval cases, golden outputs, rubric and judge graders, matrix sweeps, canary fingerprint.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,8 +22,8 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.82.0",
26
- "@rulvar/testing": "1.82.0"
25
+ "@rulvar/testing": "1.84.0",
26
+ "@rulvar/core": "1.84.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",