1use serde::{Deserialize, Serialize};
7
8const PSEUDOCOUNT: f64 = 10.0;
11
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ExtractionContext {
16 Explicit, Inferred, Speculative, Authoritative, }
21
22impl ExtractionContext {
23 #[must_use]
25 pub fn prior(self) -> f64 {
26 match self {
27 Self::Authoritative => 1.0,
28 Self::Explicit => 0.9,
29 Self::Inferred => 0.6,
30 Self::Speculative => 0.3,
31 }
32 }
33}
34
35impl std::str::FromStr for ExtractionContext {
36 type Err = String;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 match s.to_lowercase().as_str() {
40 "explicit" => Ok(Self::Explicit),
41 "inferred" => Ok(Self::Inferred),
42 "speculative" => Ok(Self::Speculative),
43 "authoritative" => Ok(Self::Authoritative),
44 other => Err(format!("unknown extraction context: {other}")),
45 }
46 }
47}
48
49#[must_use]
57pub fn bayesian_update(current_confidence: f64, corroborate: bool) -> f64 {
58 let alpha = current_confidence * PSEUDOCOUNT;
59 let beta = PSEUDOCOUNT - alpha;
60
61 if corroborate {
62 (alpha + 1.0) / (alpha + beta + 1.0)
63 } else {
64 alpha / (alpha + beta + 1.0)
65 }
66}
67
68pub const DEFAULT_HALF_LIFE_DAYS: f64 = 90.0;
71
72pub const DECAY_FLOOR: f64 = 0.05;
74
75#[must_use]
85pub fn temporal_decay(
86 stored_confidence: f64,
87 days_since_reinforced: f64,
88 half_life_days: f64,
89) -> f64 {
90 if days_since_reinforced <= 0.0 {
91 return stored_confidence;
92 }
93
94 let decay_factor = 0.5_f64.powf(days_since_reinforced / half_life_days);
95 let effective = stored_confidence * decay_factor;
96 effective.max(DECAY_FLOOR)
97}
98
99pub fn effective_confidence(
103 stored_confidence: f64,
104 last_reinforced: Option<&serde_json::Value>,
105 valid_from: &serde_json::Value,
106 now: &chrono::DateTime<chrono::Utc>,
107) -> f64 {
108 let anchor = last_reinforced
109 .and_then(parse_datetime_value)
110 .or_else(|| parse_datetime_value(valid_from));
111
112 match anchor {
113 Some(dt) => {
114 let days = (*now - dt).num_hours() as f64 / 24.0;
115 temporal_decay(stored_confidence, days, DEFAULT_HALF_LIFE_DAYS)
116 }
117 None => stored_confidence, }
119}
120
121use super::util::parse_datetime as parse_datetime_value;
122
123#[must_use]
127pub fn path_confidence(edge_confidences: &[f64]) -> f64 {
128 edge_confidences.iter().product()
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn approx_eq(a: f64, b: f64) -> bool {
136 (a - b).abs() < 0.001
137 }
138
139 #[test]
140 fn bayesian_update_corroborate_0_6() {
141 let result = bayesian_update(0.6, true);
142 assert!(approx_eq(result, 0.636), "got {}", result);
144 }
145
146 #[test]
147 fn bayesian_update_contradict_0_6() {
148 let result = bayesian_update(0.6, false);
149 assert!(approx_eq(result, 0.545), "got {}", result);
151 }
152
153 #[test]
154 fn bayesian_update_corroborate_0_9() {
155 let result = bayesian_update(0.9, true);
156 assert!(approx_eq(result, 0.909), "got {}", result);
158 }
159
160 #[test]
161 fn bayesian_update_contradict_0_9() {
162 let result = bayesian_update(0.9, false);
163 assert!(approx_eq(result, 0.818), "got {}", result);
165 }
166
167 #[test]
168 fn bayesian_update_corroborate_0_3() {
169 let result = bayesian_update(0.3, true);
170 assert!(approx_eq(result, 0.364), "got {}", result);
172 }
173
174 #[test]
175 fn path_confidence_two_edges() {
176 let result = path_confidence(&[0.8, 0.7]);
177 assert!(approx_eq(result, 0.56), "got {}", result);
178 }
179
180 #[test]
181 fn path_confidence_empty() {
182 assert_eq!(path_confidence(&[]), 1.0);
183 }
184
185 #[test]
186 fn extraction_context_priors() {
187 assert_eq!(ExtractionContext::Authoritative.prior(), 1.0);
188 assert_eq!(ExtractionContext::Explicit.prior(), 0.9);
189 assert_eq!(ExtractionContext::Inferred.prior(), 0.6);
190 assert_eq!(ExtractionContext::Speculative.prior(), 0.3);
191 }
192
193 #[test]
194 fn temporal_decay_zero_days() {
195 let result = temporal_decay(0.9, 0.0, 90.0);
196 assert!(approx_eq(result, 0.9), "got {}", result);
197 }
198
199 #[test]
200 fn temporal_decay_one_half_life() {
201 let result = temporal_decay(0.6, 90.0, 90.0);
203 assert!(approx_eq(result, 0.3), "got {}", result);
204 }
205
206 #[test]
207 fn temporal_decay_two_half_lives() {
208 let result = temporal_decay(0.8, 180.0, 90.0);
210 assert!(approx_eq(result, 0.2), "got {}", result);
211 }
212
213 #[test]
214 fn temporal_decay_floor() {
215 let result = temporal_decay(0.3, 900.0, 90.0);
217 assert!(approx_eq(result, DECAY_FLOOR), "got {}", result);
218 }
219
220 #[test]
221 fn temporal_decay_negative_days() {
222 let result = temporal_decay(0.7, -5.0, 90.0);
224 assert!(approx_eq(result, 0.7), "got {}", result);
225 }
226
227 #[test]
228 fn temporal_decay_high_confidence_still_decays() {
229 let result = temporal_decay(1.0, 90.0, 90.0);
231 assert!(approx_eq(result, 0.5), "got {}", result);
232 }
233
234 #[test]
235 fn effective_confidence_with_last_reinforced() {
236 let now = chrono::Utc::now();
237 let ninety_days_ago = (now - chrono::Duration::days(90)).to_rfc3339();
238 let valid_from_long_ago = (now - chrono::Duration::days(365)).to_rfc3339();
239
240 let last_reinforced = serde_json::Value::String(ninety_days_ago);
241 let valid_from = serde_json::Value::String(valid_from_long_ago);
242
243 let result = effective_confidence(0.6, Some(&last_reinforced), &valid_from, &now);
245 assert!(
246 approx_eq(result, 0.3),
247 "got {} (expected ~0.3, one half-life from last_reinforced)",
248 result
249 );
250 }
251
252 #[test]
253 fn effective_confidence_falls_back_to_valid_from() {
254 let now = chrono::Utc::now();
255 let ninety_days_ago = (now - chrono::Duration::days(90)).to_rfc3339();
256 let valid_from = serde_json::Value::String(ninety_days_ago);
257
258 let result = effective_confidence(0.6, None, &valid_from, &now);
260 assert!(
261 approx_eq(result, 0.3),
262 "got {} (expected ~0.3, one half-life from valid_from)",
263 result
264 );
265 }
266
267 #[test]
268 fn effective_confidence_no_parseable_date() {
269 let now = chrono::Utc::now();
270 let bad_date = serde_json::Value::String("not-a-date".to_string());
271
272 let result = effective_confidence(0.8, None, &bad_date, &now);
274 assert!(approx_eq(result, 0.8), "got {}", result);
275 }
276
277 #[test]
278 fn extraction_context_from_str() {
279 assert_eq!(
280 "explicit".parse::<ExtractionContext>().unwrap(),
281 ExtractionContext::Explicit
282 );
283 assert_eq!(
284 "inferred".parse::<ExtractionContext>().unwrap(),
285 ExtractionContext::Inferred
286 );
287 assert_eq!(
288 "speculative".parse::<ExtractionContext>().unwrap(),
289 ExtractionContext::Speculative
290 );
291 assert_eq!(
292 "authoritative".parse::<ExtractionContext>().unwrap(),
293 ExtractionContext::Authoritative
294 );
295 assert!("unknown".parse::<ExtractionContext>().is_err());
296 }
297}