Context
A Pareto-floor discipline — a proposed change may not regress on ANY measured axis even while improving another, so "trading one axis for the other" is treated as a regression, not a net win — is the correct standard for this kind of comparison. packages/loopover-engine/src/calibration/backtest-score.ts's BacktestScoreReport (#8085) gives Loopover the same two axes (precision, recall) for a candidate rule change; this issue adds the pure comparator that applies the no-regression discipline to a baseline-vs-candidate pair of reports.
Requirements
- Add a new file
packages/loopover-engine/src/calibration/backtest-compare.ts.
- Export a type:
export type BacktestComparison = {
ruleId: string;
baseline: BacktestScoreReport;
candidate: BacktestScoreReport;
regressedAxes: Array<"precision" | "recall">;
improvedAxes: Array<"precision" | "recall">;
verdict: "improved" | "regressed" | "unchanged";
};
- Export a function
compareBacktestScores(baseline: BacktestScoreReport, candidate: BacktestScoreReport): BacktestComparison:
- Throws a plain
Error (message must include both reports' ruleId values, e.g. `cannot compare backtest scores for different rules: ${baseline.ruleId} vs ${candidate.ruleId}`) when baseline.ruleId !== candidate.ruleId — comparing scores for two different rules is a caller bug, not a valid comparison.
- For each axis (
precision, recall): if either value is null, the axis is excluded from both regressedAxes and improvedAxes — a report with insufficient decided data can't be compared on that axis. Never treat null as 0 or as "no change."
- An axis is regressed when
candidate's value is strictly less than baseline's value on that axis; improved when strictly greater; when neither is null and the values are equal, the axis appears in neither list.
verdict is "regressed" whenever regressedAxes.length > 0 — a regression on even a single axis wins, regardless of whether the other axis improved. This is the Pareto-floor rule and the entire point of this function; do not compute a weighted/averaged score instead. Otherwise verdict is "improved" when improvedAxes.length > 0, else "unchanged".
Deliverables
Test Coverage Requirements
99%+ patch coverage (branch-counted). The mixed improve/regress branch described above is a required test case, not optional — a PR without it does not satisfy this issue even if aggregate coverage happens to reach 99% some other way.
Expected Outcome
Given a baseline and candidate score for the same rule, get an unambiguous improved/regressed/unchanged verdict that cannot be gamed by trading one axis for another.
Links & Resources
Context
A Pareto-floor discipline — a proposed change may not regress on ANY measured axis even while improving another, so "trading one axis for the other" is treated as a regression, not a net win — is the correct standard for this kind of comparison.
packages/loopover-engine/src/calibration/backtest-score.ts'sBacktestScoreReport(#8085) gives Loopover the same two axes (precision, recall) for a candidate rule change; this issue adds the pure comparator that applies the no-regression discipline to a baseline-vs-candidate pair of reports.Requirements
packages/loopover-engine/src/calibration/backtest-compare.ts.compareBacktestScores(baseline: BacktestScoreReport, candidate: BacktestScoreReport): BacktestComparison:Error(message must include both reports'ruleIdvalues, e.g.`cannot compare backtest scores for different rules: ${baseline.ruleId} vs ${candidate.ruleId}`) whenbaseline.ruleId !== candidate.ruleId— comparing scores for two different rules is a caller bug, not a valid comparison.precision,recall): if either value isnull, the axis is excluded from bothregressedAxesandimprovedAxes— a report with insufficient decided data can't be compared on that axis. Never treatnullas0or as "no change."candidate's value is strictly less thanbaseline's value on that axis; improved when strictly greater; when neither is null and the values are equal, the axis appears in neither list.verdictis"regressed"wheneverregressedAxes.length > 0— a regression on even a single axis wins, regardless of whether the other axis improved. This is the Pareto-floor rule and the entire point of this function; do not compute a weighted/averaged score instead. Otherwiseverdictis"improved"whenimprovedAxes.length > 0, else"unchanged".Deliverables
packages/loopover-engine/src/calibration/backtest-compare.tswithBacktestComparisonandcompareBacktestScoresas specified above.packages/loopover-engine/test/backtest-compare.test.tscovering: both axes improve →verdict: "improved",regressedAxesempty; one axis improves while the other regresses →verdict: "regressed"(the Pareto-floor case — this is the single most important test in this issue; do not omit it); one sidenullon an axis → that axis appears in neitherregressedAxesnorimprovedAxes; mismatchedruleId→ throws, and the thrown message contains both rule IDs; all axes equal →verdict: "unchanged".export * from "./calibration/backtest-compare.js";topackages/loopover-engine/src/index.ts, on its own new line immediately after theexport * from "./calibration/backtest-score.js";line added by calibration: pure confusion-matrix scorer for a candidate rule classifier against the backtest corpus #8085.Test Coverage Requirements
99%+ patch coverage (branch-counted). The mixed improve/regress branch described above is a required test case, not optional — a PR without it does not satisfy this issue even if aggregate coverage happens to reach 99% some other way.
Expected Outcome
Given a baseline and candidate score for the same rule, get an unambiguous improved/regressed/unchanged verdict that cannot be gamed by trading one axis for another.
Links & Resources