metrale_bench/benchmarks/contamination/
score.rs1use std::collections::BTreeMap;
30
31use super::transcript::{RequestOutcome, Transcript};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Class {
37 Identical,
38 Diverged {
41 at: usize,
42 detail: String,
43 },
44 Contaminated {
47 foreign: String,
48 },
49 Persistent {
52 at: usize,
53 },
54 AloneUnstable,
57 Unmeasured {
60 why: String,
61 },
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq)]
65pub struct Score {
66 pub prompts: usize,
67 pub rungs: usize,
68 pub compared: usize,
69 pub identical: usize,
70 pub diverged: usize,
71 pub contaminated: usize,
72 pub persistent: usize,
73 pub alone_unstable: usize,
74 pub unmeasured: usize,
75 pub foreign_canaries: usize,
76 pub tokens_compared: usize,
77 pub earliest_divergence: Option<usize>,
78 pub cells: BTreeMap<(usize, String), Class>,
81}
82
83fn lcp(a: &str, b: &str) -> usize {
85 a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count()
86}
87
88fn classify(
90 reference: &Transcript,
91 got: &RequestOutcome,
92 own_canary: &str,
93 all_canaries: &[&str],
94 min_completion_tokens: usize,
95 persistent_leg: bool,
96) -> Class {
97 let t = match got {
98 RequestOutcome::Error(e) => {
99 return Class::Unmeasured {
100 why: format!("request failed: {e}"),
101 };
102 }
103 RequestOutcome::Ok(t) => t,
104 };
105 if t.completion_tokens < min_completion_tokens {
108 return Class::Unmeasured {
109 why: format!(
110 "only {} completion tokens (floor {min_completion_tokens})",
111 t.completion_tokens
112 ),
113 };
114 }
115 if let Some(foreign) = t.carries_foreign_canary(own_canary, all_canaries) {
118 return Class::Contaminated {
119 foreign: foreign.to_string(),
120 };
121 }
122 let (a, b) = (reference.canonical(), t.canonical());
123 if a != b {
124 let at = lcp(&a, &b);
125 return if persistent_leg {
126 Class::Persistent { at }
127 } else {
128 Class::Diverged {
129 at,
130 detail: "stream differs from its solo reference".into(),
131 }
132 };
133 }
134 if reference.completion_tokens != t.completion_tokens {
137 return Class::Diverged {
138 at: a.chars().count(),
139 detail: format!(
140 "identical stream but completion_tokens {} vs {}",
141 reference.completion_tokens, t.completion_tokens
142 ),
143 };
144 }
145 Class::Identical
146}
147
148pub struct Legs<'a> {
150 pub ref_a: &'a [RequestOutcome],
152 pub ref_b: &'a [RequestOutcome],
153 pub rungs: &'a [(String, Vec<RequestOutcome>)],
155 pub post: &'a [RequestOutcome],
157 pub canaries: &'a [String],
158 pub min_completion_tokens: usize,
159}
160
161pub fn score(legs: &Legs) -> Score {
162 let all: Vec<&str> = legs.canaries.iter().map(String::as_str).collect();
163 let mut s = Score {
164 prompts: legs.ref_a.len(),
165 rungs: legs.rungs.len(),
166 ..Default::default()
167 };
168
169 for i in 0..legs.ref_a.len() {
170 let own = legs.canaries.get(i).map(String::as_str).unwrap_or("");
171 let (a, b) = (
174 legs.ref_a[i].transcript(),
175 legs.ref_b.get(i).and_then(RequestOutcome::transcript),
176 );
177 let reference = match (a, b) {
178 (Some(a), Some(b))
179 if a.completion_tokens < legs.min_completion_tokens
180 || b.completion_tokens < legs.min_completion_tokens =>
181 {
182 s.unmeasured += 1;
183 s.cells.insert(
184 (i, "ref".into()),
185 Class::Unmeasured {
186 why: format!(
187 "a solo reference was below the {} completion-token floor",
188 legs.min_completion_tokens
189 ),
190 },
191 );
192 continue;
193 }
194 (Some(a), Some(b))
195 if a.canonical() == b.canonical() && a.completion_tokens == b.completion_tokens =>
196 {
197 a
198 }
199 (Some(_), Some(_)) => {
200 s.alone_unstable += 1;
201 s.cells.insert((i, "ref".into()), Class::AloneUnstable);
202 continue;
203 }
204 _ => {
205 s.unmeasured += 1;
206 s.cells.insert(
207 (i, "ref".into()),
208 Class::Unmeasured {
209 why: "a solo reference leg failed".into(),
210 },
211 );
212 continue;
213 }
214 };
215
216 let mut legs_for_prompt: Vec<(String, &RequestOutcome, bool)> = legs
217 .rungs
218 .iter()
219 .filter_map(|(label, outs)| outs.get(i).map(|o| (label.clone(), o, false)))
220 .collect();
221 if let Some(p) = legs.post.get(i) {
222 legs_for_prompt.push(("post".into(), p, true));
223 }
224
225 for (label, got, persistent) in legs_for_prompt {
226 let c = classify(
227 reference,
228 got,
229 own,
230 &all,
231 legs.min_completion_tokens,
232 persistent,
233 );
234 s.compared += 1;
235 match &c {
236 Class::Identical => {
237 s.identical += 1;
238 s.tokens_compared += reference.completion_tokens;
239 }
240 Class::Diverged { at, .. } => {
241 s.diverged += 1;
242 s.earliest_divergence =
243 Some(s.earliest_divergence.map_or(*at, |e: usize| e.min(*at)));
244 }
245 Class::Persistent { at } => {
246 s.persistent += 1;
247 s.earliest_divergence =
248 Some(s.earliest_divergence.map_or(*at, |e: usize| e.min(*at)));
249 }
250 Class::Contaminated { .. } => {
251 s.contaminated += 1;
252 s.foreign_canaries += 1;
253 }
254 Class::Unmeasured { .. } => s.unmeasured += 1,
255 Class::AloneUnstable => s.alone_unstable += 1,
256 }
257 s.cells.insert((i, label), c);
258 }
259 }
260 s
261}
262
263pub fn verdict(s: &Score) -> crate::result::Verdict {
272 use crate::result::Verdict;
273 if s.compared == 0 {
274 return Verdict::fail("nothing measured: 0 comparisons (every leg failed?)");
275 }
276 let mut bad = Vec::new();
277 if s.unmeasured > 0 {
278 bad.push(format!("{} unmeasured", s.unmeasured));
279 }
280 if s.alone_unstable > 0 {
281 bad.push(format!(
282 "{} not reproducible ALONE at temp 0 (#435 class — contamination \
283 unattributable for them)",
284 s.alone_unstable
285 ));
286 }
287 if s.contaminated > 0 {
288 bad.push(format!(
289 "{} CONTAMINATED ({} foreign canaries)",
290 s.contaminated, s.foreign_canaries
291 ));
292 }
293 if s.persistent > 0 {
294 bad.push(format!("{} PERSISTENT (survived into solo)", s.persistent));
295 }
296 if s.diverged > 0 {
297 bad.push(format!("{} diverged", s.diverged));
298 }
299 if bad.is_empty() {
300 return Verdict::pass(format!(
301 "{} prompts x {} rungs + post-check: all {} streams identical to \
302 their solo reference ({} tokens compared)",
303 s.prompts, s.rungs, s.compared, s.tokens_compared
304 ));
305 }
306 let where_ = s
307 .earliest_divergence
308 .map(|c| format!("; earliest divergence at char {c}"))
309 .unwrap_or_default();
310 Verdict::fail(format!("{}{where_}", bad.join(" · ")))
311}
312
313#[cfg(test)]
314#[path = "score_tests.rs"]
315mod score_tests;