1use std::time::{Duration, SystemTime};
15
16use serde::{Deserialize, Serialize};
17
18use crate::dispute::DisputeReport;
19use crate::envelope::DealEnvelope;
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct ReputationScore {
23 pub agent: String,
24 pub completed: u32,
25 pub disputed: u32,
26 pub mean_settlement_ms: u64,
27 pub score: f32,
28}
29
30impl ReputationScore {
31 pub fn empty(agent: impl Into<String>) -> Self {
32 Self {
33 agent: agent.into(),
34 completed: 0,
35 disputed: 0,
36 mean_settlement_ms: 0,
37 score: 0.5, }
39 }
40}
41
42const HALF_LIFE_SECS: f32 = 90.0 * 24.0 * 3600.0;
43
44fn decay_weight(now: SystemTime, signed_at: SystemTime) -> f32 {
45 let age_secs = now
46 .duration_since(signed_at)
47 .map(|d| d.as_secs())
48 .unwrap_or(0) as f32;
49 0.5_f32.powf(age_secs / HALF_LIFE_SECS)
50}
51
52pub fn compute_reputation(
55 agent: &str,
56 history: &[DealEnvelope],
57 disputes: &[DisputeReport],
58) -> ReputationScore {
59 let now = SystemTime::now();
60 let agent_envelopes: Vec<&DealEnvelope> = history
61 .iter()
62 .filter(|e| e.seller == agent || e.buyer == agent)
63 .collect();
64 let completed = agent_envelopes.len() as u32;
65 let disputed = disputes.len() as u32;
66
67 if completed == 0 {
68 return ReputationScore::empty(agent);
69 }
70
71 let mut weighted_completed = 0.0f32;
72 let mut weighted_total = 0.0f32;
73 let mut settle_total = 0u64;
74 for e in &agent_envelopes {
75 let w = decay_weight(now, e.signed_at);
76 weighted_completed += w;
77 weighted_total += w;
78 settle_total += now
79 .duration_since(e.signed_at)
80 .unwrap_or(Duration::ZERO)
81 .as_millis() as u64;
82 }
83
84 let dispute_penalty = disputed as f32 * 0.10 * weighted_completed;
86 let score = if weighted_total < 1e-6 {
90 0.5
91 } else {
92 ((weighted_completed - dispute_penalty) / weighted_total).clamp(0.0, 1.0)
93 };
94
95 ReputationScore {
96 agent: agent.to_string(),
97 completed,
98 disputed,
99 mean_settlement_ms: settle_total / completed as u64,
100 score,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::envelope::DealEnvelope;
108
109 fn key() -> [u8; 32] {
110 [33u8; 32]
111 }
112
113 fn envelope(buyer: &str, seller: &str, signed_at: SystemTime) -> DealEnvelope {
114 DealEnvelope::sign(
115 buyer,
116 seller,
117 serde_json::json!({"price": "10 USDC"}),
118 [0u8; 32],
119 signed_at,
120 &key(),
121 )
122 .unwrap()
123 }
124
125 #[test]
126 fn empty_history_yields_neutral_score() {
127 let r = compute_reputation("a", &[], &[]);
128 assert_eq!(r.completed, 0);
129 assert!((r.score - 0.5).abs() < 1e-3);
130 }
131
132 #[test]
133 fn fresh_completed_deals_score_near_one() {
134 let now = SystemTime::now();
135 let history: Vec<DealEnvelope> = (0..5).map(|_| envelope("buyer", "seller", now)).collect();
136 let r = compute_reputation("seller", &history, &[]);
137 assert!(
138 r.score > 0.95,
139 "fresh agent should score >0.95, got {}",
140 r.score
141 );
142 assert_eq!(r.completed, 5);
143 }
144
145 #[test]
146 fn old_deals_decay_to_neutral_when_weight_is_tiny() {
147 let very_old = SystemTime::UNIX_EPOCH;
150 let now = SystemTime::now();
151 let new_history = vec![envelope("buyer", "seller", now)];
152 let old_history = vec![envelope("buyer", "seller", very_old)];
153 let r_new = compute_reputation("seller", &new_history, &[]);
154 let r_old = compute_reputation("seller", &old_history, &[]);
155 assert_eq!(r_new.score, 1.0);
157 assert!(
158 (r_old.score - 0.5).abs() < 1e-3,
159 "very-old deal should fall back to neutral 0.5, got {}",
160 r_old.score
161 );
162 }
163
164 #[test]
165 fn dispute_drops_score_by_at_least_ten_percent() {
166 let now = SystemTime::now();
167 let history: Vec<DealEnvelope> = (0..5).map(|_| envelope("buyer", "seller", now)).collect();
168 let no_dispute = compute_reputation("seller", &history, &[]);
169 let dispute_report = DisputeReport {
170 divergent_offset: crate::ledger::LedgerOffset(0),
171 expected_hash: [0u8; 32],
172 actual_hash: [1u8; 32],
173 };
174 let with_one = compute_reputation("seller", &history, &[dispute_report]);
175 let drop = no_dispute.score - with_one.score;
176 assert!(
177 drop >= 0.09,
178 "expected >=10% drop, got {drop} (no_dispute={}, with_one={})",
179 no_dispute.score,
180 with_one.score
181 );
182 }
183}