forge_reasoning/hypothesis/
evidence.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10use uuid::Uuid;
11
12use super::types::HypothesisId;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct EvidenceId(pub Uuid);
17
18impl EvidenceId {
19 pub fn new() -> Self {
20 Self(Uuid::new_v4())
21 }
22}
23
24impl Default for EvidenceId {
25 fn default() -> Self {
26 Self::new()
27 }
28}
29
30impl std::fmt::Display for EvidenceId {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 write!(f, "{}", self.0)
33 }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum EvidenceType {
39 Observation,
41 Experiment,
43 Reference,
45 Deduction,
47}
48
49impl EvidenceType {
50 pub fn max_strength(&self) -> f64 {
52 match self {
53 Self::Observation => 0.5,
54 Self::Experiment => 1.0,
55 Self::Reference => 0.3,
56 Self::Deduction => 0.7,
57 }
58 }
59
60 pub fn clamp_strength(&self, strength: f64) -> f64 {
62 let max = self.max_strength();
63 strength.clamp(-max, max)
64 }
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
69pub enum EvidenceMetadata {
70 Observation {
71 description: String,
72 source_path: Option<PathBuf>,
73 },
74 Experiment {
75 name: String,
76 test_command: String,
77 output: String,
78 passed: bool,
79 },
80 Reference {
81 citation: String,
82 url: Option<String>,
83 author: Option<String>,
84 },
85 Deduction {
86 premises: Vec<HypothesisId>,
87 reasoning: String,
88 },
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct Evidence {
94 pub id: EvidenceId,
95 pub evidence_type: EvidenceType,
96 pub hypothesis_id: HypothesisId,
97 pub strength: f64,
98 pub metadata: EvidenceMetadata,
99 pub created_at: DateTime<Utc>,
100}
101
102impl Evidence {
103 pub fn new(
104 hypothesis_id: HypothesisId,
105 evidence_type: EvidenceType,
106 strength: f64,
107 metadata: EvidenceMetadata,
108 ) -> Self {
109 let clamped_strength = evidence_type.clamp_strength(strength);
110
111 Self {
112 id: EvidenceId::new(),
113 evidence_type,
114 hypothesis_id,
115 strength: clamped_strength,
116 metadata,
117 created_at: Utc::now(),
118 }
119 }
120
121 pub fn id(&self) -> EvidenceId {
122 self.id
123 }
124
125 pub fn hypothesis_id(&self) -> HypothesisId {
126 self.hypothesis_id
127 }
128
129 pub fn strength(&self) -> f64 {
130 self.strength
131 }
132
133 pub fn evidence_type(&self) -> EvidenceType {
134 self.evidence_type
135 }
136
137 pub fn created_at(&self) -> DateTime<Utc> {
138 self.created_at
139 }
140
141 pub fn is_supporting(&self) -> bool {
143 self.strength > 0.0
144 }
145
146 pub fn is_refuting(&self) -> bool {
148 self.strength < 0.0
149 }
150}
151
152pub fn strength_to_likelihood(strength: f64, evidence_type: EvidenceType) -> (f64, f64) {
154 let max_strength = evidence_type.max_strength();
155 let clamped = strength.clamp(-max_strength, max_strength);
156
157 const BASE: f64 = 0.5;
159
160 let adjustment = (clamped / max_strength) * 0.4; if clamped >= 0.0 {
164 (BASE + adjustment, BASE - adjustment)
166 } else {
167 (BASE + adjustment, BASE - adjustment)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_strength_ranges() {
178 assert_eq!(EvidenceType::Observation.max_strength(), 0.5);
179 assert_eq!(EvidenceType::Experiment.max_strength(), 1.0);
180 assert_eq!(EvidenceType::Reference.max_strength(), 0.3);
181 assert_eq!(EvidenceType::Deduction.max_strength(), 0.7);
182 }
183
184 #[test]
185 fn test_strength_clamping() {
186 let evidence_type = EvidenceType::Observation;
187 assert_eq!(evidence_type.clamp_strength(1.0), 0.5); assert_eq!(evidence_type.clamp_strength(-1.0), -0.5); assert_eq!(evidence_type.clamp_strength(0.3), 0.3); }
191
192 #[test]
193 fn test_strength_to_likelihood_supporting() {
194 let (p_e_given_h, p_e_given_not_h) = strength_to_likelihood(0.9, EvidenceType::Experiment);
196 assert!(p_e_given_h > p_e_given_not_h);
197 assert!(p_e_given_h > 0.5);
198 assert!(p_e_given_not_h < 0.5);
199 }
200
201 #[test]
202 fn test_strength_to_likelihood_refuting() {
203 let (p_e_given_h, p_e_given_not_h) = strength_to_likelihood(-0.5, EvidenceType::Experiment);
205 assert!(p_e_given_h < p_e_given_not_h);
206 assert!(p_e_given_h < 0.5);
207 assert!(p_e_given_not_h > 0.5);
208 }
209
210 #[test]
211 fn test_evidence_creation_clamps_strength() {
212 let hypothesis_id = HypothesisId::new();
213 let metadata = EvidenceMetadata::Observation {
214 description: "Test".to_string(),
215 source_path: None,
216 };
217
218 let evidence = Evidence::new(
220 hypothesis_id,
221 EvidenceType::Observation,
222 1.0,
223 metadata,
224 );
225
226 assert_eq!(evidence.strength, 0.5); }
228
229 #[test]
230 fn test_evidence_id_generation() {
231 let id1 = EvidenceId::new();
232 let id2 = EvidenceId::new();
233 assert_ne!(id1, id2); }
235
236 #[test]
237 fn test_evidence_supporting_refuting() {
238 let hypothesis_id = HypothesisId::new();
239
240 let supporting = Evidence::new(
241 hypothesis_id,
242 EvidenceType::Observation,
243 0.3,
244 EvidenceMetadata::Observation {
245 description: "Supporting".to_string(),
246 source_path: None,
247 },
248 );
249 assert!(supporting.is_supporting());
250 assert!(!supporting.is_refuting());
251
252 let refuting = Evidence::new(
253 hypothesis_id,
254 EvidenceType::Observation,
255 -0.3,
256 EvidenceMetadata::Observation {
257 description: "Refuting".to_string(),
258 source_path: None,
259 },
260 );
261 assert!(!refuting.is_supporting());
262 assert!(refuting.is_refuting());
263 }
264
265 #[test]
266 fn test_strength_to_likelihood_max_experiment() {
267 let (p_e_given_h, p_e_given_not_h) = strength_to_likelihood(1.0, EvidenceType::Experiment);
269 assert!((p_e_given_h - 0.9).abs() < 1e-10); assert!((p_e_given_not_h - 0.1).abs() < 1e-10); }
272
273 #[test]
274 fn test_strength_to_likelihood_zero_strength() {
275 let (p_e_given_h, p_e_given_not_h) = strength_to_likelihood(0.0, EvidenceType::Observation);
277 assert_eq!(p_e_given_h, 0.5);
278 assert_eq!(p_e_given_not_h, 0.5);
279 }
280}