forge_reasoning/hypothesis/
mod.rs1pub mod confidence;
8pub mod types;
9pub mod evidence;
10pub mod storage;
11
12pub 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
21pub 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 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 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 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 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 pub async fn get(&self, id: HypothesisId) -> Result<Option<Hypothesis>> {
84 self.storage.get_hypothesis(id).await
85 }
86
87 pub async fn list(&self) -> Result<Vec<Hypothesis>> {
89 self.storage.list_hypotheses().await
90 }
91
92 pub async fn delete(&self, id: HypothesisId) -> Result<bool> {
94 self.storage.delete_hypothesis(id).await
95 }
96
97 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 let evidence = Evidence::new(hypothesis_id, evidence_type, strength, metadata);
112 let evidence_id = self.storage.attach_evidence(&evidence).await?;
113
114 let (likelihood_h, likelihood_not_h) =
116 strength_to_likelihood(evidence.strength(), evidence_type);
117
118 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 pub async fn get_evidence(&self, id: EvidenceId) -> Result<Option<Evidence>> {
130 self.storage.get_evidence(id).await
131 }
132
133 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 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 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 pub async fn delete_evidence(&self, id: EvidenceId) -> Result<bool> {
152 self.storage.delete_evidence(id).await
153 }
154
155 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 checkpoint_service.get_hypothesis_state(checkpoint_id).await
165 }
166
167 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 let posterior = board.update_with_evidence(id, 0.9, 0.1).await.unwrap();
212
213 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 board.set_status(id, HypothesisStatus::UnderTest).await.unwrap();
228
229 board.set_status(id, HypothesisStatus::Confirmed).await.unwrap();
231
232 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 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, metadata,
253 ).await.unwrap();
254
255 assert!(posterior.get() > 0.5);
257
258 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 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 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 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 let (evidence_id, _) = board.attach_evidence(
322 id,
323 EvidenceType::Observation,
324 1.0, metadata,
326 ).await.unwrap();
327
328 let evidence = board.get_evidence(evidence_id).await.unwrap().unwrap();
329 assert_eq!(evidence.strength(), 0.5); }
331}