Skip to main content

forge_reasoning/hypothesis/
evidence.rs

1//! Evidence attachment system for hypotheses
2//!
3//! Provides types for attaching evidence to hypotheses with four evidence types:
4//! Observation, Experiment, Reference, and Deduction. Each type has a specific
5//! strength range that maps to likelihood ratios in Bayesian updates.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10use uuid::Uuid;
11
12use super::types::HypothesisId;
13
14/// Unique identifier for evidence
15#[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/// Type of evidence with type-specific strength ranges
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum EvidenceType {
39    /// Direct observation (strength range: ±0.5)
40    Observation,
41    /// Controlled experiment (strength range: ±1.0)
42    Experiment,
43    /// External reference (strength range: ±0.3)
44    Reference,
45    /// Logical deduction from premises (strength range: ±0.7)
46    Deduction,
47}
48
49impl EvidenceType {
50    /// Maximum strength for this evidence type
51    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    /// Clamp strength to valid range for this type
61    pub fn clamp_strength(&self, strength: f64) -> f64 {
62        let max = self.max_strength();
63        strength.clamp(-max, max)
64    }
65}
66
67/// Type-specific metadata for evidence
68#[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/// Evidence attached to a hypothesis
92#[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    /// Check if evidence is supporting (positive strength)
142    pub fn is_supporting(&self) -> bool {
143        self.strength > 0.0
144    }
145
146    /// Check if evidence is refuting (negative strength)
147    pub fn is_refuting(&self) -> bool {
148        self.strength < 0.0
149    }
150}
151
152/// Convert evidence strength to likelihood ratio for Bayes update
153pub 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    // Base probability (no evidence = 0.5 for both)
158    const BASE: f64 = 0.5;
159
160    // Scale adjustment: strength moves probability away from base
161    let adjustment = (clamped / max_strength) * 0.4; // Max 0.4 adjustment
162
163    if clamped >= 0.0 {
164        // Supporting: P(E|H) > P(E|¬H)
165        (BASE + adjustment, BASE - adjustment)
166    } else {
167        // Refuting: P(E|H) < P(E|¬H) - adjustment is negative
168        (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); // Clamped down
188        assert_eq!(evidence_type.clamp_strength(-1.0), -0.5); // Clamped up
189        assert_eq!(evidence_type.clamp_strength(0.3), 0.3); // Within range
190    }
191
192    #[test]
193    fn test_strength_to_likelihood_supporting() {
194        // Strong supporting evidence
195        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        // Refuting evidence
204        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        // Try to create evidence with strength 1.0 for Observation (max is 0.5)
219        let evidence = Evidence::new(
220            hypothesis_id,
221            EvidenceType::Observation,
222            1.0,
223            metadata,
224        );
225
226        assert_eq!(evidence.strength, 0.5); // Clamped to max
227    }
228
229    #[test]
230    fn test_evidence_id_generation() {
231        let id1 = EvidenceId::new();
232        let id2 = EvidenceId::new();
233        assert_ne!(id1, id2); // UUIDs should be unique
234    }
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        // Maximum supporting strength for Experiment
268        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); // BASE + 0.4
270        assert!((p_e_given_not_h - 0.1).abs() < 1e-10); // BASE - 0.4
271    }
272
273    #[test]
274    fn test_strength_to_likelihood_zero_strength() {
275        // Zero strength should return equal probabilities
276        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}