Skip to main content

split_brain_harness/
reputation.rs

1/// Reputation scoring for capability patterns.
2///
3/// Computes a trust score and tier from accumulated `PatternMetrics`.
4/// Used by the Phase 5 `RegenerativeForge` to gate retries and blacklist
5/// patterns that have failed too many times in a row.
6use serde::{Deserialize, Serialize};
7
8use crate::tool_memory::PatternMetrics;
9
10// ---------------------------------------------------------------------------
11// Thresholds
12// ---------------------------------------------------------------------------
13
14/// Consecutive failures before a pattern is blacklisted.
15pub const BLACKLIST_THRESHOLD: u64 = 3;
16
17/// Minimum runs before a pattern can reach Trusted tier.
18const TRUSTED_MIN_RUNS: u64 = 5;
19
20/// Minimum runs + minimum trust score before a pattern is Promoted.
21const PROMOTED_MIN_RUNS: u64 = 10;
22const PROMOTED_MIN_TRUST: f64 = 0.90;
23
24// ---------------------------------------------------------------------------
25// Types
26// ---------------------------------------------------------------------------
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ReputationTier {
31    /// Fewer than 2 runs — too little data to judge.
32    Untrusted,
33    /// 2–(TRUSTED_MIN_RUNS - 1) runs, or lower trust score.
34    Emerging,
35    /// At least TRUSTED_MIN_RUNS successful runs with reasonable trust.
36    Trusted,
37    /// High-volume, high-trust pattern — may receive extra retry budget.
38    Promoted,
39    /// BLACKLIST_THRESHOLD+ consecutive failures — rejected immediately.
40    Blacklisted,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ReputationScore {
45    /// Laplace-smoothed success rate in [0.0, 1.0].
46    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
62// ---------------------------------------------------------------------------
63// Scoring
64// ---------------------------------------------------------------------------
65
66/// Compute a reputation score from accumulated pattern metrics.
67/// Call `compute_unknown()` when no metrics exist yet.
68pub 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    // Laplace smoothing: (successes + 1) / (runs + 2) avoids 0 and 1 extremes
79    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
103/// Reputation for a pattern that has never been seen before.
104pub 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// ---------------------------------------------------------------------------
114// Tests
115// ---------------------------------------------------------------------------
116
117#[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        // fail, fail, succeed — consecutive_failures resets; should NOT blacklist
158        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        // 3 successes, 3 failures — not enough to trust
181        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        // 0 successes, 0 runs → 0.5
189        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        // Even if somehow 50% success rate, 3 consecutive failures = blacklisted
197        let pm = metrics_from(&[true, true, true, false, false, false]);
198        let s = compute(&pm);
199        assert_eq!(s.tier, ReputationTier::Blacklisted);
200    }
201}