Skip to main content

forge_reasoning/belief/
graph.rs

1//! Dependency graph for beliefs (hypotheses)
2
3use petgraph::graph::{DiGraph, NodeIndex};
4use petgraph::algo::tarjan_scc;
5use petgraph::visit::Dfs;
6use std::collections::{HashMap, HashSet};
7use indexmap::IndexSet;  // For deterministic ordering
8
9use crate::hypothesis::types::HypothesisId;
10use crate::errors::Result;
11
12/// Dependency graph for beliefs (hypotheses)
13///
14/// Edge direction: A -> B means "A depends on B"
15/// So B is a "dependee" of A, and A is a "dependent" of B
16pub struct BeliefGraph {
17    graph: DiGraph<HypothesisId, ()>,
18    node_indices: HashMap<HypothesisId, NodeIndex>,
19}
20
21impl BeliefGraph {
22    pub fn new() -> Self {
23        Self {
24            graph: DiGraph::new(),
25            node_indices: HashMap::new(),
26        }
27    }
28
29    /// Add a dependency edge: hypothesis_id depends on depends_on
30    ///
31    /// Returns error if this would create a cycle
32    pub fn add_dependency(
33        &mut self,
34        hypothesis_id: HypothesisId,
35        depends_on: HypothesisId,
36    ) -> Result<()> {
37        // Check if adding this edge would create a cycle
38        // would_create_cycle returns TRUE if a cycle WOULD be created
39        if self.would_create_cycle(hypothesis_id, depends_on) {
40            return Err(crate::errors::ReasoningError::InvalidState(
41                format!("Adding dependency {} -> {} would create a cycle",
42                    hypothesis_id, depends_on)
43            ));
44        }
45
46        let from_idx = self.get_or_create_node(hypothesis_id);
47        let to_idx = self.get_or_create_node(depends_on);
48
49        // Check if edge already exists
50        if self.graph.find_edge(from_idx, to_idx).is_some() {
51            return Ok(()); // Already exists, no-op
52        }
53
54        self.graph.add_edge(from_idx, to_idx, ());
55        Ok(())
56    }
57
58    /// Remove a dependency edge
59    pub fn remove_dependency(
60        &mut self,
61        hypothesis_id: HypothesisId,
62        depends_on: HypothesisId,
63    ) -> Result<bool> {
64        let from_idx = *self.node_indices.get(&hypothesis_id)
65            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
66                format!("Hypothesis {} not found in graph", hypothesis_id)
67            ))?;
68        let to_idx = *self.node_indices.get(&depends_on)
69            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
70                format!("Hypothesis {} not found in graph", depends_on)
71            ))?;
72
73        if let Some(edge) = self.graph.find_edge(from_idx, to_idx) {
74            self.graph.remove_edge(edge);
75            Ok(true)
76        } else {
77            Ok(false)
78        }
79    }
80
81    /// Get all hypotheses that depend on the given hypothesis (incoming edges)
82    ///
83    /// If A depends on B, then A is in B's dependents list
84    pub fn dependents(&self, hypothesis_id: HypothesisId) -> Result<IndexSet<HypothesisId>> {
85        let node_idx = *self.node_indices.get(&hypothesis_id)
86            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
87                format!("Hypothesis {} not found in graph", hypothesis_id)
88            ))?;
89
90        let mut result = IndexSet::new();
91        for neighbor in self.graph.neighbors_directed(node_idx, petgraph::Direction::Incoming) {
92            result.insert(self.graph[neighbor]);
93        }
94        Ok(result)
95    }
96
97    /// Get all hypotheses that the given hypothesis depends on (outgoing edges)
98    ///
99    /// If A depends on B, then B is in A's dependees list
100    pub fn dependees(&self, hypothesis_id: HypothesisId) -> Result<IndexSet<HypothesisId>> {
101        let node_idx = *self.node_indices.get(&hypothesis_id)
102            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
103                format!("Hypothesis {} not found in graph", hypothesis_id)
104            ))?;
105
106        let mut result = IndexSet::new();
107        for neighbor in self.graph.neighbors_directed(node_idx, petgraph::Direction::Outgoing) {
108            result.insert(self.graph[neighbor]);
109        }
110        Ok(result)
111    }
112
113    /// Get full dependency chain (transitive closure) for a hypothesis
114    ///
115    /// Returns all hypotheses that this hypothesis transitively depends on
116    pub fn dependency_chain(&self, hypothesis_id: HypothesisId) -> Result<IndexSet<HypothesisId>> {
117        let node_idx = *self.node_indices.get(&hypothesis_id)
118            .ok_or_else(|| crate::errors::ReasoningError::NotFound(
119                format!("Hypothesis {} not found in graph", hypothesis_id)
120            ))?;
121
122        let mut result = IndexSet::new();
123        let mut dfs = Dfs::new(&self.graph, node_idx);
124        while let Some(reached) = dfs.next(&self.graph) {
125            if reached != node_idx {  // Exclude self
126                result.insert(self.graph[reached]);
127            }
128        }
129        Ok(result)
130    }
131
132    /// Get all hypotheses that transitively depend on this hypothesis (reverse chain)
133    pub fn reverse_dependency_chain(&self, hypothesis_id: HypothesisId) -> Result<IndexSet<HypothesisId>> {
134        // Use reversed graph for reverse reachability
135        let _reversed = self.graph.clone();
136        // Note: We need to manually collect reverse dependencies
137        let mut result = IndexSet::new();
138        let mut visited = HashSet::new();
139        self.collect_reverse_dependencies(hypothesis_id, &mut visited, &mut result);
140        result.shift_remove(&hypothesis_id); // Exclude self
141        Ok(result)
142    }
143
144    fn collect_reverse_dependencies(
145        &self,
146        hypothesis_id: HypothesisId,
147        visited: &mut HashSet<HypothesisId>,
148        result: &mut IndexSet<HypothesisId>,
149    ) {
150        if !visited.insert(hypothesis_id) {
151            return; // Already visited
152        }
153
154        result.insert(hypothesis_id);
155
156        if let Ok(direct_dependents) = self.dependents(hypothesis_id) {
157            for dependent in direct_dependents {
158                self.collect_reverse_dependencies(dependent, visited, result);
159            }
160        }
161    }
162
163    /// Detect all cycles in the belief dependency graph
164    pub fn detect_cycles(&self) -> Vec<Vec<HypothesisId>> {
165        let sccs = tarjan_scc(&self.graph);
166
167        sccs.into_iter()
168            .filter(|scc| scc.len() > 1)
169            .map(|scc| {
170                scc.into_iter()
171                    .map(|idx| self.graph[idx])
172                    .collect()
173            })
174            .collect()
175    }
176
177    /// Check if adding an edge would create a cycle
178    ///
179    /// Returns true if adding the edge WOULD create a cycle (cycle detected).
180    /// Returns false if the edge is safe to add (no cycle).
181    pub fn would_create_cycle(
182        &self,
183        hypothesis_id: HypothesisId,
184        depends_on: HypothesisId,
185    ) -> bool {
186        // Clone the graph and add the edge
187        let mut temp_graph = self.graph.clone();
188
189        // Get node indices (they're preserved in the clone)
190        let from_idx = if let Some(&idx) = self.node_indices.get(&hypothesis_id) {
191            idx
192        } else {
193            temp_graph.add_node(hypothesis_id)
194        };
195
196        let to_idx = if let Some(&idx) = self.node_indices.get(&depends_on) {
197            idx
198        } else {
199            temp_graph.add_node(depends_on)
200        };
201
202        // Add the edge we're testing
203        temp_graph.add_edge(from_idx, to_idx, ());
204
205        // A cycle is created if 'to' can reach 'from' after adding edge
206        let mut dfs = Dfs::new(&temp_graph, to_idx);
207        while let Some(reached) = dfs.next(&temp_graph) {
208            if reached == from_idx {
209                return true; // Cycle detected!
210            }
211        }
212        false
213    }
214
215    /// Get all nodes in the graph
216    pub fn nodes(&self) -> IndexSet<HypothesisId> {
217        self.node_indices.keys().copied().collect()
218    }
219
220    /// Remove a hypothesis and all its edges from the graph
221    pub fn remove_hypothesis(&mut self, hypothesis_id: HypothesisId) -> Result<bool> {
222        if let Some(node_idx) = self.node_indices.remove(&hypothesis_id) {
223            self.graph.remove_node(node_idx);
224            Ok(true)
225        } else {
226            Ok(false)
227        }
228    }
229
230    /// Returns all dependency edges as (dependent, dependee) pairs
231    ///
232    /// If A depends on B, returns (A, B)
233    pub fn all_edges(&self) -> Vec<(HypothesisId, HypothesisId)> {
234        self.graph.raw_edges()
235            .iter()
236            .map(|e| {
237                let dependent = self.graph[e.source()];
238                let dependee = self.graph[e.target()];
239                (dependent, dependee)
240            })
241            .collect()
242    }
243
244    fn get_or_create_node(&mut self, id: HypothesisId) -> NodeIndex {
245        if let Some(&idx) = self.node_indices.get(&id) {
246            return idx;
247        }
248        let idx = self.graph.add_node(id);
249        self.node_indices.insert(id, idx);
250        idx
251    }
252}
253
254impl Default for BeliefGraph {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_add_dependency() {
266        let mut graph = BeliefGraph::new();
267        let a = HypothesisId::new();
268        let b = HypothesisId::new();
269
270        graph.add_dependency(a, b).unwrap();
271        assert_eq!(graph.dependees(a).unwrap().len(), 1);
272        assert!(graph.dependees(a).unwrap().contains(&b));
273    }
274
275    #[test]
276    fn test_dependents_and_dependees() {
277        let mut graph = BeliefGraph::new();
278        let a = HypothesisId::new();
279        let b = HypothesisId::new();
280        let c = HypothesisId::new();
281
282        // A depends on B, B depends on C
283        graph.add_dependency(a, b).unwrap();
284        graph.add_dependency(b, c).unwrap();
285
286        // A's dependees: {B}
287        assert_eq!(graph.dependees(a).unwrap().len(), 1);
288        assert!(graph.dependees(a).unwrap().contains(&b));
289
290        // B's dependents: {A}
291        assert_eq!(graph.dependents(b).unwrap().len(), 1);
292        assert!(graph.dependents(b).unwrap().contains(&a));
293
294        // C's dependents: {B}
295        assert_eq!(graph.dependents(c).unwrap().len(), 1);
296        assert!(graph.dependents(c).unwrap().contains(&b));
297    }
298
299    #[test]
300    fn test_dependency_chain() {
301        let mut graph = BeliefGraph::new();
302        let a = HypothesisId::new();
303        let b = HypothesisId::new();
304        let c = HypothesisId::new();
305        let d = HypothesisId::new();
306
307        // A depends on B, B depends on C, C depends on D
308        graph.add_dependency(a, b).unwrap();
309        graph.add_dependency(b, c).unwrap();
310        graph.add_dependency(c, d).unwrap();
311
312        // A's dependency chain: {B, C, D}
313        let chain = graph.dependency_chain(a).unwrap();
314        assert_eq!(chain.len(), 3);
315        assert!(chain.contains(&b));
316        assert!(chain.contains(&c));
317        assert!(chain.contains(&d));
318    }
319
320    #[test]
321    fn test_cycle_detection_prevents_cycle() {
322        let mut graph = BeliefGraph::new();
323        let a = HypothesisId::new();
324        let b = HypothesisId::new();
325        let c = HypothesisId::new();
326
327        graph.add_dependency(a, b).unwrap();
328        graph.add_dependency(b, c).unwrap();
329
330        // Adding C -> A would create a cycle
331        let result = graph.add_dependency(c, a);
332        assert!(result.is_err());
333    }
334
335    #[test]
336    fn test_detect_existing_cycles() {
337        let mut graph = BeliefGraph::new();
338        let a = HypothesisId::new();
339        let b = HypothesisId::new();
340
341        // Create a cycle directly (bypassing add_dependency check for testing)
342        graph.get_or_create_node(a);
343        graph.get_or_create_node(b);
344        let from_idx = *graph.node_indices.get(&a).unwrap();
345        let to_idx = *graph.node_indices.get(&b).unwrap();
346        graph.graph.add_edge(from_idx, to_idx, ());
347        graph.graph.add_edge(to_idx, from_idx, ());
348
349        let cycles = graph.detect_cycles();
350        assert_eq!(cycles.len(), 1);
351    }
352}