use serde::{Deserialize, Serialize};
use crate::tool_memory::PatternMetrics;
pub const BLACKLIST_THRESHOLD: u64 = 3;
const TRUSTED_MIN_RUNS: u64 = 5;
const PROMOTED_MIN_RUNS: u64 = 10;
const PROMOTED_MIN_TRUST: f64 = 0.90;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReputationTier {
Untrusted,
Emerging,
Trusted,
Promoted,
Blacklisted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReputationScore {
pub trust: f64,
pub tier: ReputationTier,
pub total_runs: u64,
pub consecutive_failures: u64,
}
impl ReputationScore {
pub fn is_blacklisted(&self) -> bool {
self.tier == ReputationTier::Blacklisted
}
pub fn is_promoted(&self) -> bool {
self.tier == ReputationTier::Promoted
}
}
pub fn compute(metrics: &PatternMetrics) -> ReputationScore {
if metrics.consecutive_failures >= BLACKLIST_THRESHOLD {
return ReputationScore {
trust: 0.0,
tier: ReputationTier::Blacklisted,
total_runs: metrics.runs,
consecutive_failures: metrics.consecutive_failures,
};
}
let trust = if metrics.runs == 0 {
0.5
} else {
(metrics.successes as f64 + 1.0) / (metrics.runs as f64 + 2.0)
};
let tier = if metrics.runs < 2 {
ReputationTier::Untrusted
} else if trust >= PROMOTED_MIN_TRUST && metrics.runs >= PROMOTED_MIN_RUNS {
ReputationTier::Promoted
} else if trust > 0.5 && metrics.runs >= TRUSTED_MIN_RUNS {
ReputationTier::Trusted
} else {
ReputationTier::Emerging
};
ReputationScore {
trust,
tier,
total_runs: metrics.runs,
consecutive_failures: metrics.consecutive_failures,
}
}
pub fn compute_unknown() -> ReputationScore {
ReputationScore {
trust: 0.5,
tier: ReputationTier::Untrusted,
total_runs: 0,
consecutive_failures: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capability::ToolMetrics;
fn metrics_from(runs: &[bool]) -> PatternMetrics {
let mut pm = PatternMetrics::default();
for &success in runs {
pm.record(&ToolMetrics {
success,
..Default::default()
});
}
pm
}
#[test]
fn unknown_is_untrusted_not_blacklisted() {
let s = compute_unknown();
assert_eq!(s.tier, ReputationTier::Untrusted);
assert!(!s.is_blacklisted());
assert!((s.trust - 0.5).abs() < 1e-9);
}
#[test]
fn zero_runs_is_untrusted() {
let s = compute(&PatternMetrics::default());
assert_eq!(s.tier, ReputationTier::Untrusted);
}
#[test]
fn three_consecutive_failures_blacklists() {
let pm = metrics_from(&[false, false, false]);
let s = compute(&pm);
assert_eq!(s.tier, ReputationTier::Blacklisted);
assert!(s.is_blacklisted());
}
#[test]
fn success_after_two_failures_resets_blacklist_timer() {
let pm = metrics_from(&[false, false, true]);
let s = compute(&pm);
assert_ne!(s.tier, ReputationTier::Blacklisted);
}
#[test]
fn five_successes_reaches_trusted() {
let pm = metrics_from(&[true, true, true, true, true]);
let s = compute(&pm);
assert_eq!(s.tier, ReputationTier::Trusted);
}
#[test]
fn ten_successes_reaches_promoted() {
let pm = metrics_from(&[true, true, true, true, true, true, true, true, true, true]);
let s = compute(&pm);
assert_eq!(s.tier, ReputationTier::Promoted);
assert!(s.is_promoted());
}
#[test]
fn mixed_history_stays_emerging() {
let pm = metrics_from(&[true, false, true, false, true, false]);
let s = compute(&pm);
assert_eq!(s.tier, ReputationTier::Emerging);
}
#[test]
fn trust_is_laplace_smoothed() {
let pm = PatternMetrics::default();
let s = compute(&pm);
assert!((s.trust - 0.5).abs() < 1e-9);
}
#[test]
fn blacklist_check_precedes_trust_calculation() {
let pm = metrics_from(&[true, true, true, false, false, false]);
let s = compute(&pm);
assert_eq!(s.tier, ReputationTier::Blacklisted);
}
}