metrale_bench/benchmarks/ssm_poison/
score.rs1use crate::result::Verdict;
13
14use super::compare::{RoundVerdict, TurnDelta};
15
16#[derive(Debug, Clone)]
19pub struct RoundRecord {
20 pub round: usize,
22 pub verdict: RoundVerdict,
23 pub turn1_cached: Option<usize>,
27}
28
29#[derive(Debug, Clone)]
31pub struct Score {
32 pub rounds: usize,
33 pub invariant: usize,
34 pub jittered: usize,
35 pub collapsed: usize,
36 pub unmeasured: usize,
37 pub jittered_rounds: Vec<(usize, Vec<TurnDelta>)>,
39 pub collapsed_rounds: Vec<(usize, Vec<TurnDelta>)>,
41 pub unmeasured_rounds: Vec<(usize, String)>,
44 pub vacuous_rounds: Vec<usize>,
46 pub min_turn1_cached: Option<usize>,
49}
50
51pub(super) fn score(replays: &[RoundRecord]) -> Score {
53 let count = |f: fn(&RoundVerdict) -> bool| replays.iter().filter(|r| f(&r.verdict)).count();
54 let jittered_rounds = replays
55 .iter()
56 .filter_map(|r| {
57 if let RoundVerdict::Jittered { turns } = &r.verdict {
58 Some((r.round, turns.clone()))
59 } else {
60 None
61 }
62 })
63 .collect();
64 let collapsed_rounds = replays
65 .iter()
66 .filter_map(|r| {
67 if let RoundVerdict::Collapsed { turns } = &r.verdict {
68 Some((r.round, turns.clone()))
69 } else {
70 None
71 }
72 })
73 .collect();
74 let unmeasured_rounds = replays
75 .iter()
76 .filter_map(|r| {
77 if let RoundVerdict::Unmeasured { reason } = &r.verdict {
78 Some((r.round, reason.clone()))
79 } else {
80 None
81 }
82 })
83 .collect();
84 let vacuous_rounds = replays
85 .iter()
86 .filter(|r| r.turn1_cached == Some(0))
87 .map(|r| r.round)
88 .collect();
89 let min_turn1_cached = replays.iter().filter_map(|r| r.turn1_cached).min();
90 Score {
91 rounds: replays.len(),
92 invariant: count(|v| matches!(v, RoundVerdict::Invariant)),
93 jittered: count(|v| matches!(v, RoundVerdict::Jittered { .. })),
94 collapsed: count(|v| matches!(v, RoundVerdict::Collapsed { .. })),
95 unmeasured: count(|v| matches!(v, RoundVerdict::Unmeasured { .. })),
96 jittered_rounds,
97 collapsed_rounds,
98 unmeasured_rounds,
99 vacuous_rounds,
100 min_turn1_cached,
101 }
102}
103
104fn turn_summary(turns: &[TurnDelta]) -> String {
105 turns
106 .iter()
107 .map(|t| {
108 format!(
109 "turn {} ({} -> {} tokens, finish {:?} -> {:?})",
110 t.turn, t.ref_tokens, t.replay_tokens, t.ref_finish, t.replay_finish
111 )
112 })
113 .collect::<Vec<_>>()
114 .join("; ")
115}
116
117pub(super) fn verdict(s: &Score, rounds: usize) -> Verdict {
125 if s.rounds != rounds {
126 return Verdict::fail(format!(
127 "{} of {} replay rounds completed",
128 s.rounds, rounds
129 ));
130 }
131 if s.collapsed > 0 {
132 let detail = s
133 .collapsed_rounds
134 .iter()
135 .map(|(n, turns)| format!("round {n}: {}", turn_summary(turns)))
136 .collect::<Vec<_>>()
137 .join(" | ");
138 return Verdict::fail(format!(
139 "{} of {} replays COLLAPSED against the reference: {detail} — a restored prefix \
140 produced degenerate output (early-EOS or runaway), the SSM state poisoning \
141 signature",
142 s.collapsed, rounds
143 ));
144 }
145 if s.unmeasured > 0 {
146 let detail = s
147 .unmeasured_rounds
148 .iter()
149 .map(|(round, reason)| format!("round {round}: {reason}"))
150 .collect::<Vec<_>>()
151 .join(" | ");
152 return Verdict::fail(format!(
153 "{} of {} replays were unmeasurable (transport errors): {detail} — the replay \
154 invariant is unproven for those rounds",
155 s.unmeasured, rounds,
156 ));
157 }
158 if !s.vacuous_rounds.is_empty() {
159 return Verdict::fail(format!(
160 "replay round(s) {:?} attested 0 cached prompt tokens on turn 1 — the prefix \
161 restore path this gate polices was never exercised, so their transcripts prove \
162 nothing about the poisoning class (is prefix caching enabled on the served \
163 recipe?)",
164 s.vacuous_rounds
165 ));
166 }
167 if s.jittered > 0 {
168 return Verdict::pass(format!(
169 "{} of {} replays byte-identical, {} jittered within bounds (restore anchor \
170 selection varies between rounds on a healthy engine), 0 collapsed",
171 s.invariant, s.rounds, s.jittered
172 ));
173 }
174 Verdict::pass(format!(
175 "{} of {} replays byte-identical to the reference",
176 s.invariant, rounds
177 ))
178}
179
180#[cfg(test)]
181#[path = "score_tests.rs"]
182mod score_tests;