1use serde::{Deserialize, Serialize};
22
23use super::result::{CandidateSource, TuneCandidateResult};
24use crate::domain::benchmark::agentic::{EFFECT_NOISE_RATIO, PairedEffect};
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(tag = "verdict", rename_all = "snake_case")]
33pub enum ApplyVerdict {
34 Apply {
37 winner_composite: f64,
39 incumbent_mean: f64,
41 margin: f64,
43 drift: f64,
45 paired: Option<PairedEffect>,
47 },
48 IncumbentStands {
52 incumbent_mean: f64,
54 },
55 WithinDrift {
59 margin: f64,
61 drift: f64,
63 },
64 PairedDisagrees {
69 wins: usize,
71 losses: usize,
73 },
74 Uncalibrated,
78 Contaminated {
82 unmeasured_runs: usize,
84 },
85}
86
87impl ApplyVerdict {
88 #[must_use]
90 pub const fn applies(&self) -> bool {
91 matches!(self, Self::Apply { .. })
92 }
93
94 #[must_use]
106 pub const fn rationale(&self) -> &'static str {
107 match self {
108 Self::Apply { .. } => "The margin cleared the run's own drift and the pairs agreed.",
109 Self::IncumbentStands { .. } => {
110 "No candidate beat the model's current defaults. The run answered its \
111 question, and the answer is 'change nothing'."
112 }
113 Self::WithinDrift { .. } => {
114 "The winner's margin is inside the run's own drift. Unresolved, not \
115 absent; more tasks or a re-run resolves it, a smaller threshold never \
116 does."
117 }
118 Self::PairedDisagrees { .. } => {
119 "The winner's mean rests on a minority of tasks — the lucky-outlier \
120 shape, refused by the pairs."
121 }
122 Self::Uncalibrated => {
123 "This run has no incumbent calibration pair, so nothing measures its \
124 drift. Re-run the tune; every new run carries the pair."
125 }
126 Self::Contaminated { .. } => {
127 "Some task runs never reached the model, so the compared scores are \
128 contaminated. A zero from a dead upstream is not a low score."
129 }
130 }
131 }
132}
133
134impl std::fmt::Display for ApplyVerdict {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 match self {
142 Self::Apply {
143 winner_composite,
144 incumbent_mean,
145 margin,
146 drift,
147 ..
148 } => write!(
149 f,
150 "applied: winner {winner_composite:.3} over incumbent \
151 {incumbent_mean:.3}, margin {margin:+.3} against drift {drift:.3}"
152 ),
153 Self::IncumbentStands { incumbent_mean } => {
154 write!(f, "refused: incumbent stands at {incumbent_mean:.3}")
155 }
156 Self::WithinDrift { margin, drift } => {
157 write!(f, "refused: margin {margin:+.3} within drift {drift:.3}")
158 }
159 Self::PairedDisagrees { wins, losses } => {
160 write!(f, "refused: pairs disagree ({wins}W-{losses}L)")
161 }
162 Self::Uncalibrated => write!(f, "refused: uncalibrated run"),
163 Self::Contaminated { unmeasured_runs } => {
164 write!(f, "refused: {unmeasured_runs} unmeasured run(s)")
165 }
166 }
167 }
168}
169
170#[must_use]
176pub fn evaluate_apply(candidates: &[TuneCandidateResult]) -> ApplyVerdict {
177 let incumbents: Vec<&TuneCandidateResult> = candidates
178 .iter()
179 .filter(|c| {
180 matches!(
181 c.source,
182 CandidateSource::Incumbent | CandidateSource::IncumbentCalibration
183 ) && !c.pruned
184 })
185 .collect();
186 let [first, second] = incumbents.as_slice() else {
187 return ApplyVerdict::Uncalibrated;
188 };
189
190 let Some(winner) = candidates
191 .iter()
192 .filter(|c| {
193 !c.pruned
194 && !matches!(
195 c.source,
196 CandidateSource::Incumbent | CandidateSource::IncumbentCalibration
197 )
198 })
199 .max_by(|a, b| {
200 a.composite_score
201 .partial_cmp(&b.composite_score)
202 .expect("composites are finite")
203 })
204 else {
205 return ApplyVerdict::IncumbentStands {
208 incumbent_mean: f64::midpoint(first.composite_score, second.composite_score),
209 };
210 };
211
212 let unmeasured_runs = unmeasured(winner) + unmeasured(first) + unmeasured(second);
213 if unmeasured_runs > 0 {
214 return ApplyVerdict::Contaminated { unmeasured_runs };
215 }
216
217 let incumbent_mean = f64::midpoint(first.composite_score, second.composite_score);
218 let drift = (first.composite_score - second.composite_score).abs();
219 let margin = winner.composite_score - incumbent_mean;
220
221 if margin <= 0.0 {
222 return ApplyVerdict::IncumbentStands { incumbent_mean };
223 }
224 if margin < EFFECT_NOISE_RATIO * drift {
225 return ApplyVerdict::WithinDrift { margin, drift };
226 }
227
228 let paired = PairedEffect::from_paired_runs(&winner.task_results, &first.task_results);
233 if let Some(p) = &paired
234 && p.losses > p.wins
235 {
236 return ApplyVerdict::PairedDisagrees {
237 wins: p.wins,
238 losses: p.losses,
239 };
240 }
241
242 ApplyVerdict::Apply {
243 winner_composite: winner.composite_score,
244 incumbent_mean,
245 margin,
246 drift,
247 paired,
248 }
249}
250
251#[must_use]
256pub fn winning_candidate(candidates: &[TuneCandidateResult]) -> Option<&TuneCandidateResult> {
257 candidates
258 .iter()
259 .filter(|c| {
260 !c.pruned
261 && !matches!(
262 c.source,
263 CandidateSource::Incumbent | CandidateSource::IncumbentCalibration
264 )
265 })
266 .max_by(|a, b| {
267 a.composite_score
268 .partial_cmp(&b.composite_score)
269 .expect("composites are finite")
270 })
271}
272
273fn unmeasured(candidate: &TuneCandidateResult) -> usize {
274 candidate
275 .task_results
276 .iter()
277 .filter(|r| !r.is_measured())
278 .count()
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct ApplyRecord {
286 pub verdict: ApplyVerdict,
288 #[serde(default)]
293 pub applied_config: Option<crate::domain::InferenceConfig>,
294 #[serde(default)]
302 pub prior_defaults: Option<Option<crate::domain::InferenceConfig>>,
303 #[serde(default)]
305 pub prior_origin: Option<Option<crate::domain::DefaultsOrigin>>,
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::domain::InferenceConfig;
312 use crate::domain::benchmark::tune::result::{GeneratedOutput, TuneTaskResult};
313 use crate::domain::benchmark::tune::task::TaskCategory;
314
315 fn task(id: &str, score: f64) -> TuneTaskResult {
316 TuneTaskResult {
317 task_id: id.to_owned(),
318 category: TaskCategory::SingleCall,
319 passed: score >= 1.0,
320 tool_match_score: score,
321 loop_detected: false,
322 stagnation_detected: false,
323 iterations: 1,
324 latency_ms: 10,
325 completion_tokens: Some(100),
326 time_to_first_tool_call_ms: Some(5),
327 detail: None,
328 unmeasured: None,
329 transport_retries: 0,
330 generated: GeneratedOutput::default(),
331 }
332 }
333
334 fn candidate(source: CandidateSource, scores: &[f64]) -> TuneCandidateResult {
335 #[allow(clippy::cast_precision_loss)]
336 let composite = scores.iter().sum::<f64>() / scores.len() as f64;
337 TuneCandidateResult {
338 config: InferenceConfig::default(),
339 source,
340 task_results: scores
341 .iter()
342 .enumerate()
343 .map(|(i, s)| task(&format!("t{i}"), *s))
344 .collect(),
345 composite_score: composite,
346 pruned: false,
347 tg_tps: None,
348 }
349 }
350
351 fn incumbent_pair(scores: &[f64], twin_scores: &[f64]) -> Vec<TuneCandidateResult> {
352 vec![
353 candidate(CandidateSource::Incumbent, scores),
354 candidate(CandidateSource::IncumbentCalibration, twin_scores),
355 ]
356 }
357
358 #[test]
361 fn a_clear_winner_applies() {
362 let mut candidates = incumbent_pair(&[0.5, 0.5, 0.5], &[0.52, 0.5, 0.5]);
363 candidates.push(candidate(CandidateSource::UserGrid, &[0.9, 0.9, 0.9]));
364 let verdict = evaluate_apply(&candidates);
365 assert!(verdict.applies(), "{verdict:?}");
366 }
367
368 #[test]
371 fn a_margin_within_drift_is_refused() {
372 let mut candidates = incumbent_pair(&[0.5, 0.5, 0.6], &[0.8, 0.8, 0.9]);
374 candidates.push(candidate(CandidateSource::UserGrid, &[0.9, 0.8, 0.9]));
376 match evaluate_apply(&candidates) {
377 ApplyVerdict::WithinDrift { margin, drift } => {
378 assert!(margin < EFFECT_NOISE_RATIO * drift, "{margin} vs {drift}");
379 }
380 other => panic!("expected WithinDrift, got {other:?}"),
381 }
382 }
383
384 #[test]
387 fn a_run_without_the_incumbent_pair_is_uncalibrated() {
388 let candidates = vec![candidate(CandidateSource::UserGrid, &[1.0, 1.0, 1.0])];
389 assert_eq!(evaluate_apply(&candidates), ApplyVerdict::Uncalibrated);
390 }
391
392 #[test]
395 fn an_unbeaten_incumbent_stands() {
396 let mut candidates = incumbent_pair(&[0.9, 0.9, 0.9], &[0.9, 0.9, 0.9]);
397 candidates.push(candidate(CandidateSource::UserGrid, &[0.5, 0.5, 0.5]));
398 assert!(matches!(
399 evaluate_apply(&candidates),
400 ApplyVerdict::IncumbentStands { .. }
401 ));
402 }
403
404 #[test]
408 fn contaminated_candidates_are_refused() {
409 let mut candidates = incumbent_pair(&[0.5, 0.5, 0.5], &[0.5, 0.5, 0.5]);
410 let mut winner = candidate(CandidateSource::UserGrid, &[0.9, 0.9, 0.9]);
411 winner.task_results[1].unmeasured = Some("upstream died".to_owned());
412 candidates.push(winner);
413 assert!(matches!(
414 evaluate_apply(&candidates),
415 ApplyVerdict::Contaminated { unmeasured_runs: 1 }
416 ));
417 }
418
419 #[test]
422 fn a_minority_winner_is_refused_by_the_pairs() {
423 let mut candidates = incumbent_pair(&[0.5, 0.5, 0.5, 0.5], &[0.5, 0.5, 0.5, 0.5]);
427 candidates.push(candidate(CandidateSource::UserGrid, &[1.0, 0.4, 0.4, 0.4]));
428 match evaluate_apply(&candidates) {
429 ApplyVerdict::PairedDisagrees { wins, losses } => {
430 assert_eq!((wins, losses), (1, 3));
431 }
432 other => panic!("expected PairedDisagrees, got {other:?}"),
433 }
434 }
435
436 #[test]
439 fn a_pruned_candidate_cannot_win() {
440 let mut candidates = incumbent_pair(&[0.5, 0.5, 0.5], &[0.5, 0.5, 0.5]);
441 let mut pruned = candidate(CandidateSource::UserGrid, &[1.0, 1.0, 1.0]);
442 pruned.pruned = true;
443 candidates.push(pruned);
444 assert!(matches!(
445 evaluate_apply(&candidates),
446 ApplyVerdict::IncumbentStands { .. }
447 ));
448 }
449}