split_brain_harness/
reputation.rs1use serde::{Deserialize, Serialize};
7
8use crate::tool_memory::PatternMetrics;
9
10pub const BLACKLIST_THRESHOLD: u64 = 3;
16
17const TRUSTED_MIN_RUNS: u64 = 5;
19
20const PROMOTED_MIN_RUNS: u64 = 10;
22const PROMOTED_MIN_TRUST: f64 = 0.90;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ReputationTier {
31 Untrusted,
33 Emerging,
35 Trusted,
37 Promoted,
39 Blacklisted,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ReputationScore {
45 pub trust: f64,
47 pub tier: ReputationTier,
48 pub total_runs: u64,
49 pub consecutive_failures: u64,
50}
51
52impl ReputationScore {
53 pub fn is_blacklisted(&self) -> bool {
54 self.tier == ReputationTier::Blacklisted
55 }
56
57 pub fn is_promoted(&self) -> bool {
58 self.tier == ReputationTier::Promoted
59 }
60}
61
62pub fn compute(metrics: &PatternMetrics) -> ReputationScore {
69 if metrics.consecutive_failures >= BLACKLIST_THRESHOLD {
70 return ReputationScore {
71 trust: 0.0,
72 tier: ReputationTier::Blacklisted,
73 total_runs: metrics.runs,
74 consecutive_failures: metrics.consecutive_failures,
75 };
76 }
77
78 let trust = if metrics.runs == 0 {
80 0.5
81 } else {
82 (metrics.successes as f64 + 1.0) / (metrics.runs as f64 + 2.0)
83 };
84
85 let tier = if metrics.runs < 2 {
86 ReputationTier::Untrusted
87 } else if trust >= PROMOTED_MIN_TRUST && metrics.runs >= PROMOTED_MIN_RUNS {
88 ReputationTier::Promoted
89 } else if trust > 0.5 && metrics.runs >= TRUSTED_MIN_RUNS {
90 ReputationTier::Trusted
91 } else {
92 ReputationTier::Emerging
93 };
94
95 ReputationScore {
96 trust,
97 tier,
98 total_runs: metrics.runs,
99 consecutive_failures: metrics.consecutive_failures,
100 }
101}
102
103pub fn compute_unknown() -> ReputationScore {
105 ReputationScore {
106 trust: 0.5,
107 tier: ReputationTier::Untrusted,
108 total_runs: 0,
109 consecutive_failures: 0,
110 }
111}
112
113#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::capability::ToolMetrics;
121
122 fn metrics_from(runs: &[bool]) -> PatternMetrics {
123 let mut pm = PatternMetrics::default();
124 for &success in runs {
125 pm.record(&ToolMetrics {
126 success,
127 ..Default::default()
128 });
129 }
130 pm
131 }
132
133 #[test]
134 fn unknown_is_untrusted_not_blacklisted() {
135 let s = compute_unknown();
136 assert_eq!(s.tier, ReputationTier::Untrusted);
137 assert!(!s.is_blacklisted());
138 assert!((s.trust - 0.5).abs() < 1e-9);
139 }
140
141 #[test]
142 fn zero_runs_is_untrusted() {
143 let s = compute(&PatternMetrics::default());
144 assert_eq!(s.tier, ReputationTier::Untrusted);
145 }
146
147 #[test]
148 fn three_consecutive_failures_blacklists() {
149 let pm = metrics_from(&[false, false, false]);
150 let s = compute(&pm);
151 assert_eq!(s.tier, ReputationTier::Blacklisted);
152 assert!(s.is_blacklisted());
153 }
154
155 #[test]
156 fn success_after_two_failures_resets_blacklist_timer() {
157 let pm = metrics_from(&[false, false, true]);
159 let s = compute(&pm);
160 assert_ne!(s.tier, ReputationTier::Blacklisted);
161 }
162
163 #[test]
164 fn five_successes_reaches_trusted() {
165 let pm = metrics_from(&[true, true, true, true, true]);
166 let s = compute(&pm);
167 assert_eq!(s.tier, ReputationTier::Trusted);
168 }
169
170 #[test]
171 fn ten_successes_reaches_promoted() {
172 let pm = metrics_from(&[true, true, true, true, true, true, true, true, true, true]);
173 let s = compute(&pm);
174 assert_eq!(s.tier, ReputationTier::Promoted);
175 assert!(s.is_promoted());
176 }
177
178 #[test]
179 fn mixed_history_stays_emerging() {
180 let pm = metrics_from(&[true, false, true, false, true, false]);
182 let s = compute(&pm);
183 assert_eq!(s.tier, ReputationTier::Emerging);
184 }
185
186 #[test]
187 fn trust_is_laplace_smoothed() {
188 let pm = PatternMetrics::default();
190 let s = compute(&pm);
191 assert!((s.trust - 0.5).abs() < 1e-9);
192 }
193
194 #[test]
195 fn blacklist_check_precedes_trust_calculation() {
196 let pm = metrics_from(&[true, true, true, false, false, false]);
198 let s = compute(&pm);
199 assert_eq!(s.tier, ReputationTier::Blacklisted);
200 }
201}