evidential_protocol/
scoring.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ScoringAxis {
16 pub name: String,
18 pub weight: f64,
20 pub value: f64,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ContinuousScore {
31 pub entity_id: String,
33 pub axes: Vec<ScoringAxis>,
35 pub composite_score: f64,
37 pub confidence: f64,
39 pub assessed_at: String,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ScoringEngine {
50 axes: Vec<(String, f64)>,
52}
53
54impl Default for ScoringEngine {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl ScoringEngine {
61 pub fn new() -> Self {
63 Self { axes: Vec::new() }
64 }
65
66 pub fn define_axes(&mut self, axes: Vec<(&str, f64)>) {
70 self.axes = axes
71 .into_iter()
72 .map(|(name, weight)| (name.to_string(), weight))
73 .collect();
74 }
75
76 pub fn score(&self, entity_id: &str, values: &HashMap<String, f64>) -> ContinuousScore {
82 let total_weight: f64 = self.axes.iter().map(|(_, w)| w).sum();
83 let mut weighted_sum = 0.0_f64;
84 let mut axes = Vec::new();
85 let mut provided_count = 0_usize;
86
87 for (name, weight) in &self.axes {
88 let value = values.get(name).copied().unwrap_or(0.0);
89 if values.contains_key(name) {
90 provided_count += 1;
91 }
92 weighted_sum += value * weight;
93 axes.push(ScoringAxis {
94 name: name.clone(),
95 weight: *weight,
96 value,
97 });
98 }
99
100 let composite_score = if total_weight > 0.0 {
101 (weighted_sum / total_weight).clamp(0.0, 1.0)
102 } else {
103 0.0
104 };
105
106 let confidence = if self.axes.is_empty() {
107 0.0
108 } else {
109 provided_count as f64 / self.axes.len() as f64
110 };
111
112 ContinuousScore {
113 entity_id: entity_id.to_string(),
114 axes,
115 composite_score,
116 confidence,
117 assessed_at: chrono::Utc::now().to_rfc3339(),
118 }
119 }
120
121 pub fn rank(scores: &mut [ContinuousScore]) {
123 scores.sort_by(|a, b| {
124 b.composite_score
125 .partial_cmp(&a.composite_score)
126 .unwrap_or(std::cmp::Ordering::Equal)
127 });
128 }
129
130 pub fn get_outliers(
133 scores: &[ContinuousScore],
134 threshold: f64,
135 ) -> Vec<&ContinuousScore> {
136 if scores.is_empty() {
137 return Vec::new();
138 }
139
140 let mean: f64 =
141 scores.iter().map(|s| s.composite_score).sum::<f64>() / scores.len() as f64;
142
143 scores
144 .iter()
145 .filter(|s| (s.composite_score - mean).abs() > threshold)
146 .collect()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn basic_scoring() {
156 let mut engine = ScoringEngine::new();
157 engine.define_axes(vec![("accuracy", 0.6), ("speed", 0.4)]);
158
159 let mut values = HashMap::new();
160 values.insert("accuracy".to_string(), 0.9);
161 values.insert("speed".to_string(), 0.7);
162
163 let score = engine.score("agent-1", &values);
164 assert!((score.composite_score - 0.82).abs() < 1e-10);
166 assert!((score.confidence - 1.0).abs() < 1e-10);
167 assert_eq!(score.axes.len(), 2);
168 }
169
170 #[test]
171 fn partial_values() {
172 let mut engine = ScoringEngine::new();
173 engine.define_axes(vec![("a", 0.5), ("b", 0.5)]);
174
175 let mut values = HashMap::new();
176 values.insert("a".to_string(), 0.8);
177 let score = engine.score("x", &values);
180 assert!((score.composite_score - 0.4).abs() < 1e-10);
182 assert!((score.confidence - 0.5).abs() < 1e-10);
183 }
184
185 #[test]
186 fn ranking() {
187 let mut engine = ScoringEngine::new();
188 engine.define_axes(vec![("score", 1.0)]);
189
190 let mut scores: Vec<ContinuousScore> = (1..=5)
191 .map(|i| {
192 let mut v = HashMap::new();
193 v.insert("score".to_string(), i as f64 * 0.2);
194 engine.score(&format!("e-{}", i), &v)
195 })
196 .collect();
197
198 ScoringEngine::rank(&mut scores);
199 assert_eq!(scores[0].entity_id, "e-5");
200 assert_eq!(scores[4].entity_id, "e-1");
201 }
202
203 #[test]
204 fn outlier_detection() {
205 let mut engine = ScoringEngine::new();
206 engine.define_axes(vec![("val", 1.0)]);
207
208 let scores: Vec<ContinuousScore> = vec![0.5, 0.5, 0.5, 0.5, 0.95]
209 .into_iter()
210 .enumerate()
211 .map(|(i, v)| {
212 let mut vals = HashMap::new();
213 vals.insert("val".to_string(), v);
214 engine.score(&format!("e-{}", i), &vals)
215 })
216 .collect();
217
218 let outliers = ScoringEngine::get_outliers(&scores, 0.3);
220 assert_eq!(outliers.len(), 1);
221 assert_eq!(outliers[0].entity_id, "e-4");
222 }
223}