oris_evolution/
confidence.rs1use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use crate::core::{
10 AssetState, Capsule, GeneId, MIN_REPLAY_CONFIDENCE, REPLAY_CONFIDENCE_DECAY_RATE_PER_HOUR,
11};
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct ConfidenceSchedulerConfig {
16 pub check_interval_secs: u64,
18 pub confidence_boost_per_success: f32,
20 pub max_confidence: f32,
22 pub enabled: bool,
24}
25
26impl Default for ConfidenceSchedulerConfig {
27 fn default() -> Self {
28 Self {
29 check_interval_secs: 3600, confidence_boost_per_success: 0.1,
31 max_confidence: 1.0,
32 enabled: true,
33 }
34 }
35}
36
37#[derive(Clone, Debug)]
39pub enum ConfidenceAction {
40 DecayCapsule {
42 capsule_id: String,
43 gene_id: GeneId,
44 old_confidence: f32,
45 new_confidence: f32,
46 },
47 DemoteToQuarantined { asset_id: String, confidence: f32 },
49 BoostConfidence {
51 asset_id: String,
52 old_confidence: f32,
53 new_confidence: f32,
54 },
55}
56
57#[derive(Error, Debug)]
59pub enum SchedulerError {
60 #[error("Scheduler not running")]
61 NotRunning,
62
63 #[error("IO error: {0}")]
64 IoError(String),
65
66 #[error("Store error: {0}")]
67 StoreError(String),
68}
69
70pub trait ConfidenceScheduler: Send + Sync {
72 fn apply_decay_to_capsule(&self, capsule_confidence: f32, age_hours: f32) -> f32;
74
75 fn boost_confidence(&self, current: f32) -> f32;
77
78 fn should_quarantine(&self, confidence: f32) -> bool;
80}
81
82pub struct StandardConfidenceScheduler {
84 config: ConfidenceSchedulerConfig,
85}
86
87impl StandardConfidenceScheduler {
88 pub fn new(config: ConfidenceSchedulerConfig) -> Self {
89 Self { config }
90 }
91
92 pub fn with_default_config() -> Self {
93 Self::new(ConfidenceSchedulerConfig::default())
94 }
95
96 pub fn calculate_decay(confidence: f32, hours: f32) -> f32 {
98 if confidence <= 0.0 {
99 return 0.0;
100 }
101 let decay = (-REPLAY_CONFIDENCE_DECAY_RATE_PER_HOUR * hours).exp();
102 (confidence * decay).clamp(0.0, 1.0)
103 }
104
105 pub fn calculate_age_hours(created_at_ms: i64, now_ms: i64) -> f32 {
107 let diff_ms = now_ms - created_at_ms;
108 let diff_secs = diff_ms / 1000;
109 diff_secs as f32 / 3600.0
110 }
111}
112
113impl ConfidenceScheduler for StandardConfidenceScheduler {
114 fn apply_decay_to_capsule(&self, capsule_confidence: f32, age_hours: f32) -> f32 {
115 Self::calculate_decay(capsule_confidence, age_hours)
116 }
117
118 fn boost_confidence(&self, current: f32) -> f32 {
119 let new_confidence = current + self.config.confidence_boost_per_success;
120 new_confidence.min(self.config.max_confidence)
121 }
122
123 fn should_quarantine(&self, confidence: f32) -> bool {
124 confidence < MIN_REPLAY_CONFIDENCE
125 }
126}
127
128#[derive(Clone, Debug, Default, Serialize, Deserialize)]
130pub struct ConfidenceMetrics {
131 pub decay_checks_total: u64,
132 pub capsules_decayed_total: u64,
133 pub capsules_quarantined_total: u64,
134 pub confidence_boosts_total: u64,
135}
136
137pub fn process_capsule_confidence(
139 scheduler: &dyn ConfidenceScheduler,
140 capsule_id: &str,
141 gene_id: &GeneId,
142 confidence: f32,
143 created_at_ms: i64,
144 current_time_ms: i64,
145 state: AssetState,
146) -> Vec<ConfidenceAction> {
147 let mut actions = Vec::new();
148
149 if state != AssetState::Promoted {
151 return actions;
152 }
153
154 let age_hours =
155 StandardConfidenceScheduler::calculate_age_hours(created_at_ms, current_time_ms);
156
157 if age_hours > 0.0 {
158 let old_conf = confidence;
159 let new_conf = scheduler.apply_decay_to_capsule(old_conf, age_hours);
160
161 if (new_conf - old_conf).abs() > 0.001 {
162 actions.push(ConfidenceAction::DecayCapsule {
163 capsule_id: capsule_id.to_string(),
164 gene_id: gene_id.clone(),
165 old_confidence: old_conf,
166 new_confidence: new_conf,
167 });
168 }
169
170 if scheduler.should_quarantine(new_conf) {
172 actions.push(ConfidenceAction::DemoteToQuarantined {
173 asset_id: capsule_id.to_string(),
174 confidence: new_conf,
175 });
176 }
177 }
178
179 actions
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_calculate_decay() {
188 let conf = StandardConfidenceScheduler::calculate_decay(1.0, 0.0);
190 assert!((conf - 1.0).abs() < 0.001);
191
192 let conf = StandardConfidenceScheduler::calculate_decay(1.0, 13.86);
194 assert!((conf - 0.5).abs() < 0.01);
195
196 let conf = StandardConfidenceScheduler::calculate_decay(1.0, 24.0);
198 assert!((conf - 0.30).abs() < 0.02);
199
200 let conf = StandardConfidenceScheduler::calculate_decay(0.0, 100.0);
202 assert!(conf.abs() < 0.001);
203 }
204
205 #[test]
206 fn test_should_quarantine() {
207 let scheduler = StandardConfidenceScheduler::with_default_config();
208
209 assert!(!scheduler.should_quarantine(0.5));
211 assert!(!scheduler.should_quarantine(0.35));
212 assert!(!scheduler.should_quarantine(0.36));
213
214 assert!(scheduler.should_quarantine(0.34));
216 assert!(scheduler.should_quarantine(0.0));
217 }
218
219 #[test]
220 fn test_boost_confidence() {
221 let scheduler = StandardConfidenceScheduler::with_default_config();
222
223 let conf = scheduler.boost_confidence(0.5);
225 assert!((conf - 0.6).abs() < 0.001);
226
227 let conf = scheduler.boost_confidence(1.0);
229 assert!((conf - 1.0).abs() < 0.001);
230
231 let conf = scheduler.boost_confidence(0.95);
233 assert!((conf - 1.0).abs() < 0.001);
234 }
235
236 #[test]
237 fn test_default_config() {
238 let config = ConfidenceSchedulerConfig::default();
239 assert!(config.enabled);
240 assert_eq!(config.check_interval_secs, 3600);
241 assert!((config.confidence_boost_per_success - 0.1).abs() < 0.001);
242 }
243
244 #[test]
245 fn test_calculate_age_hours() {
246 let age = StandardConfidenceScheduler::calculate_age_hours(0, 3600000);
248 assert!((age - 1.0).abs() < 0.001);
249
250 let age = StandardConfidenceScheduler::calculate_age_hours(0, 86400000);
252 assert!((age - 24.0).abs() < 0.001);
253
254 let age = StandardConfidenceScheduler::calculate_age_hours(0, 1800000);
256 assert!((age - 0.5).abs() < 0.001);
257 }
258}