//! Inference Engine (Phase 5.2) //! //! Rule-based inference with graph traversal, transitive closure, and //! confidence propagation through reasoning chains. use std::collections::{HashMap, HashSet, VecDeque}; use std::pin::Pin; use std::future::Future; use sqlx::PgPool; use serde::{Deserialize, Serialize}; use tracing::{debug, warn}; /// Inference rule #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InferenceRule { /// Rule ID pub id: String, /// Antecedent predicate (e.g., "depends_on") pub antecedent: String, /// Medial predicate (optional, for chain rules) pub medial: Option, /// Consequent predicate (e.g., "related_to") pub consequent: String, /// Confidence multiplier (0.0-1.0) pub confidence_multiplier: f32, /// Description pub description: String, } /// Inferred fact #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct InferredFact { /// Source entity ID pub source_id: String, /// Source entity name pub source_name: String, /// Target entity ID pub target_id: String, /// Target entity name pub target_name: String, /// Inferred relation type pub relation_type: String, /// Confidence (0.0-1.0) pub confidence: f32, /// Reasoning chain that led to inference pub reasoning_chain: Vec, /// Rule IDs applied pub rule_ids: Vec, } /// Reasoning path #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReasoningPath { /// Path steps: entity_id → entity_id → ... pub path: Vec, /// Relations between steps: relation_type → relation_type → ... pub relations: Vec, /// Accumulated confidence (product of step confidences) pub confidence: f32, /// Steps in path pub step_count: usize, } /// Transitive closure result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransitiveClosure { /// Starting entity ID pub source_id: String, /// All reachable entities with relation type and confidence pub reachable: Vec, /// Total entities reached pub entity_count: usize, /// Total edges in closure pub edge_count: usize, } /// Reachable entity info #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReachableEntity { /// Entity ID pub entity_id: String, /// Entity name pub entity_name: String, /// Relation type from source pub relation_type: String, /// Combined confidence pub confidence: f32, /// Hop distance from source pub distance: usize, } /// Inference Engine pub struct InferenceEngine { pool: PgPool, rules: Vec, } impl InferenceEngine { pub fn new(pool: PgPool, rules: Vec) -> Self { InferenceEngine { pool, rules } } /// Perform rule-based inference /// /// Applies inference rules to graph, generating new facts pub async fn infer_facts( &self, project_id: &str, entity_id: &str, max_hops: usize, ) -> Result, String> { if entity_id.is_empty() || max_hops == 0 { return Ok(vec![]); } let mut inferred = Vec::new(); let mut visited = HashSet::new(); // BFS from entity_id applying rules at each step let mut queue = VecDeque::new(); queue.push_back((entity_id.to_string(), 0, 1.0, vec![])); while let Some((current_id, depth, confidence, chain)) = queue.pop_front() { if depth >= max_hops || visited.contains(¤t_id) { continue; } visited.insert(current_id.clone()); // Get edges from current entity let edges = self.fetch_entity_edges(¤t_id, project_id).await?; for edge in edges { // Apply each rule for rule in &self.rules { if edge.relation_type == rule.antecedent { let new_confidence = (confidence * rule.confidence_multiplier).min(1.0); if new_confidence > 0.1 { let mut new_chain = chain.clone(); new_chain.push(format!("{} --{}→ {}", current_id, rule.consequent, edge.target_id)); inferred.push(InferredFact { source_id: entity_id.to_string(), source_name: "Unknown".to_string(), target_id: edge.target_id.clone(), target_name: edge.target_name.clone(), relation_type: rule.consequent.clone(), confidence: new_confidence, reasoning_chain: new_chain.clone(), rule_ids: vec![rule.id.clone()], }); queue.push_back(( edge.target_id.clone(), depth + 1, new_confidence, new_chain, )); } } } } } // Deduplicate by (source, target, relation) let mut deduped: HashMap<(String, String, String), InferredFact> = HashMap::new(); for fact in inferred { let key = (fact.source_id.clone(), fact.target_id.clone(), fact.relation_type.clone()); deduped.entry(key).or_insert(fact); } Ok(deduped.into_values().collect()) } /// Compute transitive closure for entity pub async fn transitive_closure( &self, entity_id: &str, project_id: &str, relation_type: Option<&str>, max_hops: usize, ) -> Result { let mut reachable = Vec::new(); let mut visited: HashMap = HashMap::new(); let mut queue = VecDeque::new(); queue.push_back((entity_id.to_string(), 1.0, 0)); visited.insert(entity_id.to_string(), (1.0, 0)); while let Some((current_id, confidence, distance)) = queue.pop_front() { if distance >= max_hops { continue; } let edges = self.fetch_entity_edges(¤t_id, project_id).await?; for edge in edges { // Filter by relation type if specified if let Some(rel_type) = relation_type { if edge.relation_type != rel_type { continue; } } let new_confidence = confidence * 0.95; // Decay confidence per hop let target = edge.target_id.clone(); let entry = visited.entry(target.clone()).or_insert((new_confidence, distance + 1)); // Keep higher confidence path if new_confidence > entry.0 { entry.0 = new_confidence; entry.1 = distance + 1; reachable.push(ReachableEntity { entity_id: target.clone(), entity_name: edge.target_name.clone(), relation_type: edge.relation_type.clone(), confidence: new_confidence, distance: distance + 1, }); queue.push_back((target, new_confidence, distance + 1)); } } } let edge_count = reachable.len(); let entity_count = visited.len() - 1; // Exclude starting entity Ok(TransitiveClosure { source_id: entity_id.to_string(), reachable, entity_count, edge_count, }) } /// Find all reasoning paths between entities pub async fn find_reasoning_paths( &self, source_id: &str, target_id: &str, project_id: &str, max_hops: usize, ) -> Result, String> { let mut paths = Vec::new(); let mut visited = HashSet::new(); self.dfs_paths( source_id, target_id, project_id, max_hops, &mut vec![source_id.to_string()], &mut vec![], &mut vec![1.0], &mut visited, &mut paths, ).await?; Ok(paths) } /// Check if fact can be inferred from rules pub fn check_inference_validity( &self, antecedent: &str, consequent: &str, ) -> Option<(String, f32)> { for rule in &self.rules { if rule.antecedent == antecedent && rule.consequent == consequent { return Some((rule.id.clone(), rule.confidence_multiplier)); } } None } /// Get applicable rules for relation type pub fn get_applicable_rules(&self, relation_type: &str) -> Vec<&InferenceRule> { self.rules.iter().filter(|r| r.antecedent == relation_type).collect() } // ========== Private Helper Methods ========== /// Fetch edges from entity async fn fetch_entity_edges( &self, entity_id: &str, project_id: &str, ) -> Result, String> { // Stub: would query database Ok(vec![]) } /// DFS to find all paths fn dfs_paths<'a>( &'a self, current: &'a str, target: &'a str, project_id: &'a str, remaining_hops: usize, path: &'a mut Vec, relations: &'a mut Vec, confidences: &'a mut Vec, visited: &'a mut HashSet, results: &'a mut Vec, ) -> Pin> + Send + 'a>> { Box::pin(async move { if remaining_hops == 0 { return Ok(()); } if current == target && path.len() > 1 { let confidence = confidences.iter().product(); results.push(ReasoningPath { path: path.clone(), relations: relations.clone(), confidence, step_count: path.len(), }); return Ok(()); } let edges = self.fetch_entity_edges(current, project_id).await?; for edge in edges { if !visited.contains(&edge.target_id) { visited.insert(edge.target_id.clone()); path.push(edge.target_id.clone()); relations.push(edge.relation_type.clone()); confidences.push(0.9); // Nominal confidence per edge self.dfs_paths( &edge.target_id, target, project_id, remaining_hops - 1, path, relations, confidences, visited, results, ).await?; path.pop(); relations.pop(); confidences.pop(); visited.remove(&edge.target_id); } } Ok(()) }) // Box::pin } } /// Internal edge info struct EdgeInfo { source_id: String, target_id: String, target_name: String, relation_type: String, }