Skip to main content

forge_reasoning/hypothesis/
mod.rs

1//! Hypothesis management with Bayesian confidence tracking
2//!
3//! This module provides tools for proposing, tracking, and updating hypotheses
4//! during debugging. LLMs can maintain explicit belief states with proper
5//! Bayesian updates as evidence accumulates.
6
7pub mod confidence;
8pub mod types;
9pub mod evidence;
10pub mod storage;
11
12// Public exports
13pub use confidence::{Confidence, ConfidenceError};
14pub use types::{Hypothesis, HypothesisId, HypothesisStatus, HypothesisState};
15pub use storage::{HypothesisStorage, InMemoryHypothesisStorage};
16pub use evidence::{Evidence, EvidenceId, EvidenceType, EvidenceMetadata, strength_to_likelihood};
17
18use std::sync::Arc;
19use crate::errors::Result;
20
21/// Main API for hypothesis management
22pub struct HypothesisBoard {
23    storage: Arc<dyn HypothesisStorage>,
24}
25
26impl HypothesisBoard {
27    pub fn new(storage: Arc<dyn HypothesisStorage>) -> Self {
28        Self { storage }
29    }
30
31    pub fn in_memory() -> Self {
32        Self::new(Arc::new(InMemoryHypothesisStorage::new()))
33    }
34
35    /// Propose a new hypothesis with explicit prior
36    pub async fn propose(
37        &self,
38        statement: impl Into<String>,
39        prior: Confidence,
40    ) -> Result<HypothesisId> {
41        let hypothesis = Hypothesis::new(statement, prior);
42        self.storage.create_hypothesis(&hypothesis).await
43    }
44
45    /// Propose with maximum uncertainty (0.5) convenience method
46    pub async fn propose_with_max_uncertainty(
47        &self,
48        statement: impl Into<String>,
49    ) -> Result<HypothesisId> {
50        self.propose(statement, Confidence::max_uncertainty()).await
51    }
52
53    /// Update hypothesis confidence using Bayes formula
54    pub async fn update_with_evidence(
55        &self,
56        id: HypothesisId,
57        likelihood_h: f64,
58        likelihood_not_h: f64,
59    ) -> Result<Confidence> {
60        let hypothesis = self.storage.get_hypothesis(id).await?
61            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
62                format!("Hypothesis {} not found", id)
63            ))?;
64
65        let current = hypothesis.current_confidence();
66        let posterior = current.update_with_evidence(likelihood_h, likelihood_not_h)
67            .map_err(|e| crate::errors::ReasoningError::InvalidState(e.to_string()))?;
68
69        self.storage.update_confidence(id, posterior).await?;
70        Ok(posterior)
71    }
72
73    /// Update hypothesis status
74    pub async fn set_status(
75        &self,
76        id: HypothesisId,
77        status: HypothesisStatus,
78    ) -> Result<()> {
79        self.storage.set_status(id, status).await
80    }
81
82    /// Get a hypothesis by ID
83    pub async fn get(&self, id: HypothesisId) -> Result<Option<Hypothesis>> {
84        self.storage.get_hypothesis(id).await
85    }
86
87    /// List all hypotheses
88    pub async fn list(&self) -> Result<Vec<Hypothesis>> {
89        self.storage.list_hypotheses().await
90    }
91
92    /// Delete a hypothesis
93    pub async fn delete(&self, id: HypothesisId) -> Result<bool> {
94        self.storage.delete_hypothesis(id).await
95    }
96
97    /// Attach evidence to a hypothesis and update confidence
98    ///
99    /// This is the primary method for evidence attachment. It:
100    /// 1. Stores the evidence
101    /// 2. Converts strength to likelihood ratio
102    /// 3. Updates hypothesis confidence using Bayes formula
103    pub async fn attach_evidence(
104        &self,
105        hypothesis_id: HypothesisId,
106        evidence_type: EvidenceType,
107        strength: f64,
108        metadata: EvidenceMetadata,
109    ) -> Result<(EvidenceId, Confidence)> {
110        // Create evidence
111        let evidence = Evidence::new(hypothesis_id, evidence_type, strength, metadata);
112        let evidence_id = self.storage.attach_evidence(&evidence).await?;
113
114        // Convert strength to likelihood ratio
115        let (likelihood_h, likelihood_not_h) =
116            strength_to_likelihood(evidence.strength(), evidence_type);
117
118        // Update hypothesis confidence
119        let posterior = self.update_with_evidence(
120            hypothesis_id,
121            likelihood_h,
122            likelihood_not_h,
123        ).await?;
124
125        Ok((evidence_id, posterior))
126    }
127
128    /// Get evidence by ID
129    pub async fn get_evidence(&self, id: EvidenceId) -> Result<Option<Evidence>> {
130        self.storage.get_evidence(id).await
131    }
132
133    /// List all evidence for a hypothesis
134    pub async fn list_evidence(&self, hypothesis_id: HypothesisId) -> Result<Vec<Evidence>> {
135        self.storage.list_evidence_for_hypothesis(hypothesis_id).await
136    }
137
138    /// Trace supporting evidence for a hypothesis
139    pub async fn list_supporting_evidence(&self, hypothesis_id: HypothesisId) -> Result<Vec<Evidence>> {
140        let all = self.list_evidence(hypothesis_id).await?;
141        Ok(all.into_iter().filter(|e| e.is_supporting()).collect())
142    }
143
144    /// Trace refuting evidence for a hypothesis
145    pub async fn list_refuting_evidence(&self, hypothesis_id: HypothesisId) -> Result<Vec<Evidence>> {
146        let all = self.list_evidence(hypothesis_id).await?;
147        Ok(all.into_iter().filter(|e| e.is_refuting()).collect())
148    }
149
150    /// Delete evidence
151    pub async fn delete_evidence(&self, id: EvidenceId) -> Result<bool> {
152        self.storage.delete_evidence(id).await
153    }
154
155    /// Query hypothesis state at a past checkpoint time
156    ///
157    /// This enables time-travel queries: "What did I believe at checkpoint X?"
158    pub async fn state_at(
159        &self,
160        checkpoint_service: &crate::service::CheckpointService,
161        checkpoint_id: crate::checkpoint::CheckpointId,
162    ) -> Result<Option<crate::hypothesis::types::HypothesisState>> {
163        // Query checkpoint service for hypothesis state at given checkpoint
164        checkpoint_service.get_hypothesis_state(checkpoint_id).await
165    }
166
167    /// Directly sets confidence without evidence (used by propagation system)
168    ///
169    /// This method bypasses the normal evidence-based update path and is intended
170    /// for use by the confidence propagation system, which updates dependents
171    /// based on cascade computations.
172    pub async fn update_confidence_direct(
173        &self,
174        id: HypothesisId,
175        confidence: Confidence,
176    ) -> Result<()> {
177        self.storage.update_confidence(id, confidence).await
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[tokio::test]
186    async fn test_propose_hypothesis() {
187        let board = HypothesisBoard::in_memory();
188        let prior = Confidence::new(0.5).unwrap();
189        let id = board.propose("Test hypothesis", prior).await.unwrap();
190        assert!(board.get(id).await.unwrap().is_some());
191    }
192
193    #[tokio::test]
194    async fn test_confidence_rejects_nan() {
195        assert!(Confidence::new(f64::NAN).is_err());
196    }
197
198    #[tokio::test]
199    async fn test_confidence_rejects_out_of_bounds() {
200        assert!(Confidence::new(1.5).is_err());
201        assert!(Confidence::new(-0.1).is_err());
202    }
203
204    #[tokio::test]
205    async fn test_bayes_update() {
206        let board = HypothesisBoard::in_memory();
207        let prior = Confidence::new(0.5).unwrap();
208        let id = board.propose("Test", prior).await.unwrap();
209
210        // Supporting evidence: P(E|H) = 0.9, P(E|¬H) = 0.1
211        let posterior = board.update_with_evidence(id, 0.9, 0.1).await.unwrap();
212
213        // Posterior should be higher than prior (0.5 -> ~0.9)
214        assert!(posterior.get() > 0.8);
215
216        let h = board.get(id).await.unwrap().unwrap();
217        assert_eq!(h.posterior(), posterior);
218    }
219
220    #[tokio::test]
221    async fn test_status_transitions() {
222        let board = HypothesisBoard::in_memory();
223        let prior = Confidence::new(0.5).unwrap();
224        let id = board.propose("Test", prior).await.unwrap();
225
226        // Valid: Proposed -> UnderTest
227        board.set_status(id, HypothesisStatus::UnderTest).await.unwrap();
228
229        // Valid: UnderTest -> Confirmed
230        board.set_status(id, HypothesisStatus::Confirmed).await.unwrap();
231
232        // Invalid: Confirmed -> Proposed (should fail)
233        assert!(board.set_status(id, HypothesisStatus::Proposed).await.is_err());
234    }
235
236    #[tokio::test]
237    async fn test_attach_evidence_updates_confidence() {
238        let board = HypothesisBoard::in_memory();
239        let prior = Confidence::new(0.5).unwrap();
240        let id = board.propose("Test hypothesis", prior).await.unwrap();
241
242        // Attach supporting evidence
243        let metadata = EvidenceMetadata::Observation {
244            description: "Observed behavior supports hypothesis".to_string(),
245            source_path: None,
246        };
247
248        let (evidence_id, posterior) = board.attach_evidence(
249            id,
250            EvidenceType::Observation,
251            0.5,  // Max supporting strength for Observation
252            metadata,
253        ).await.unwrap();
254
255        // Posterior should be higher than prior
256        assert!(posterior.get() > 0.5);
257
258        // Evidence should be retrievable
259        let evidence = board.get_evidence(evidence_id).await.unwrap().unwrap();
260        assert_eq!(evidence.hypothesis_id(), id);
261    }
262
263    #[tokio::test]
264    async fn test_list_evidence_by_hypothesis() {
265        let board = HypothesisBoard::in_memory();
266        let prior = Confidence::new(0.5).unwrap();
267        let id = board.propose("Test", prior).await.unwrap();
268
269        // Attach multiple evidence
270        for i in 0..3 {
271            let metadata = EvidenceMetadata::Observation {
272                description: format!("Observation {}", i),
273                source_path: None,
274            };
275            board.attach_evidence(id, EvidenceType::Observation, 0.3, metadata).await.unwrap();
276        }
277
278        let evidence_list = board.list_evidence(id).await.unwrap();
279        assert_eq!(evidence_list.len(), 3);
280    }
281
282    #[tokio::test]
283    async fn test_supporting_vs_refuting_evidence() {
284        let board = HypothesisBoard::in_memory();
285        let prior = Confidence::new(0.5).unwrap();
286        let id = board.propose("Test", prior).await.unwrap();
287
288        // Supporting evidence
289        let metadata_support = EvidenceMetadata::Observation {
290            description: "Supporting".to_string(),
291            source_path: None,
292        };
293        board.attach_evidence(id, EvidenceType::Observation, 0.4, metadata_support).await.unwrap();
294
295        // Refuting evidence
296        let metadata_refute = EvidenceMetadata::Observation {
297            description: "Refuting".to_string(),
298            source_path: None,
299        };
300        board.attach_evidence(id, EvidenceType::Observation, -0.4, metadata_refute).await.unwrap();
301
302        let supporting = board.list_supporting_evidence(id).await.unwrap();
303        let refuting = board.list_refuting_evidence(id).await.unwrap();
304
305        assert_eq!(supporting.len(), 1);
306        assert_eq!(refuting.len(), 1);
307    }
308
309    #[tokio::test]
310    async fn test_evidence_strength_clamping_by_type() {
311        let board = HypothesisBoard::in_memory();
312        let prior = Confidence::new(0.5).unwrap();
313        let id = board.propose("Test", prior).await.unwrap();
314
315        let metadata = EvidenceMetadata::Observation {
316            description: "Test".to_string(),
317            source_path: None,
318        };
319
320        // Try to attach evidence with strength 1.0 (beyond Observation's max of 0.5)
321        let (evidence_id, _) = board.attach_evidence(
322            id,
323            EvidenceType::Observation,
324            1.0,  // Will be clamped to 0.5
325            metadata,
326        ).await.unwrap();
327
328        let evidence = board.get_evidence(evidence_id).await.unwrap().unwrap();
329        assert_eq!(evidence.strength(), 0.5); // Clamped
330    }
331}