rust_rule_engine/rete/
propagation.rs

1//! Incremental Propagation Engine (P3 Feature - Advanced)
2//!
3//! This module implements incremental updates similar to Drools:
4//! - Only propagate changed facts through the network
5//! - Track affected rules and activations
6//! - Efficient re-evaluation after updates
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10use super::working_memory::{WorkingMemory, FactHandle};
11use super::network::{ReteUlNode, TypedReteUlRule};
12use super::facts::{TypedFacts, FactValue};
13use super::agenda::{AdvancedAgenda, Activation};
14use super::template::TemplateRegistry;
15use super::globals::GlobalsRegistry;
16use super::deffacts::DeffactsRegistry;
17use super::tms::TruthMaintenanceSystem;
18use crate::errors::{Result, RuleEngineError};
19
20/// Track which rules are affected by which fact types
21#[derive(Debug)]
22pub struct RuleDependencyGraph {
23    /// Map: fact_type -> set of rule indices that depend on it
24    fact_type_to_rules: HashMap<String, HashSet<usize>>,
25    /// Map: rule index -> set of fact types it depends on
26    rule_to_fact_types: HashMap<usize, HashSet<String>>,
27}
28
29impl RuleDependencyGraph {
30    /// Create new dependency graph
31    pub fn new() -> Self {
32        Self {
33            fact_type_to_rules: HashMap::new(),
34            rule_to_fact_types: HashMap::new(),
35        }
36    }
37
38    /// Add dependency: rule depends on fact type
39    pub fn add_dependency(&mut self, rule_idx: usize, fact_type: String) {
40        self.fact_type_to_rules
41            .entry(fact_type.clone())
42            .or_insert_with(HashSet::new)
43            .insert(rule_idx);
44
45        self.rule_to_fact_types
46            .entry(rule_idx)
47            .or_insert_with(HashSet::new)
48            .insert(fact_type);
49    }
50
51    /// Get rules affected by a fact type change
52    pub fn get_affected_rules(&self, fact_type: &str) -> HashSet<usize> {
53        self.fact_type_to_rules
54            .get(fact_type)
55            .cloned()
56            .unwrap_or_else(HashSet::new)
57    }
58
59    /// Get fact types that a rule depends on
60    pub fn get_rule_dependencies(&self, rule_idx: usize) -> HashSet<String> {
61        self.rule_to_fact_types
62            .get(&rule_idx)
63            .cloned()
64            .unwrap_or_else(HashSet::new)
65    }
66}
67
68impl Default for RuleDependencyGraph {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74/// Type alias for custom test functions in RETE engine
75/// Functions take a slice of FactValues and return a FactValue (typically Boolean)
76pub type ReteCustomFunction = Arc<dyn Fn(&[FactValue], &TypedFacts) -> Result<FactValue> + Send + Sync>;
77
78/// Incremental Propagation Engine
79/// Only re-evaluates rules affected by changed facts
80pub struct IncrementalEngine {
81    /// Working memory
82    working_memory: WorkingMemory,
83    /// Rules
84    rules: Vec<TypedReteUlRule>,
85    /// Dependency graph
86    dependencies: RuleDependencyGraph,
87    /// Advanced agenda
88    agenda: AdvancedAgenda,
89    /// Track which facts each rule last matched
90    rule_matched_facts: HashMap<usize, HashSet<FactHandle>>,
91    /// Template registry for type-safe facts
92    templates: TemplateRegistry,
93    /// Global variables registry
94    globals: GlobalsRegistry,
95    /// Deffacts registry for initial facts
96    deffacts: DeffactsRegistry,
97    /// Custom functions for Test CE support
98    custom_functions: HashMap<String, ReteCustomFunction>,
99    /// Truth Maintenance System
100    tms: TruthMaintenanceSystem,
101}
102
103impl IncrementalEngine {
104    /// Create new incremental engine
105    pub fn new() -> Self {
106        Self {
107            working_memory: WorkingMemory::new(),
108            rules: Vec::new(),
109            dependencies: RuleDependencyGraph::new(),
110            agenda: AdvancedAgenda::new(),
111            rule_matched_facts: HashMap::new(),
112            custom_functions: HashMap::new(),
113            templates: TemplateRegistry::new(),
114            globals: GlobalsRegistry::new(),
115            deffacts: DeffactsRegistry::new(),
116            tms: TruthMaintenanceSystem::new(),
117        }
118    }
119
120    /// Add rule and register its dependencies
121    pub fn add_rule(&mut self, rule: TypedReteUlRule, depends_on: Vec<String>) {
122        let rule_idx = self.rules.len();
123
124        // Register dependencies
125        for fact_type in depends_on {
126            self.dependencies.add_dependency(rule_idx, fact_type);
127        }
128
129        self.rules.push(rule);
130    }
131
132    /// Insert fact into working memory
133    pub fn insert(&mut self, fact_type: String, data: TypedFacts) -> FactHandle {
134        let handle = self.working_memory.insert(fact_type.clone(), data);
135        
136        // Default: Treat as explicit assertion (backward compatible)
137        self.tms.add_explicit_justification(handle);
138
139        // Trigger incremental propagation for this fact type
140        self.propagate_changes_for_type(&fact_type);
141
142        handle
143    }
144
145    /// Update fact in working memory
146    pub fn update(&mut self, handle: FactHandle, data: TypedFacts) -> Result<()> {
147        // Get fact type before update
148        let fact_type = self.working_memory
149            .get(&handle)
150            .map(|f| f.fact_type.clone())
151            .ok_or_else(|| RuleEngineError::FieldNotFound {
152                field: format!("FactHandle {} not found", handle),
153            })?;
154
155        self.working_memory.update(handle, data).map_err(|e| RuleEngineError::EvaluationError {
156            message: e,
157        })?;
158
159        // Trigger incremental propagation for this fact type
160        self.propagate_changes_for_type(&fact_type);
161
162        Ok(())
163    }
164
165    /// Retract fact from working memory
166    pub fn retract(&mut self, handle: FactHandle) -> Result<()> {
167        // Get fact type before retract
168        let fact_type = self.working_memory
169            .get(&handle)
170            .map(|f| f.fact_type.clone())
171            .ok_or_else(|| RuleEngineError::FieldNotFound {
172                field: format!("FactHandle {} not found", handle),
173            })?;
174
175        self.working_memory.retract(handle).map_err(|e| RuleEngineError::EvaluationError {
176            message: e,
177        })?;
178
179        // TMS: Handle cascade retraction
180        let cascaded_facts = self.tms.retract_with_cascade(handle);
181        
182        // Actually retract cascaded facts from working memory
183        for cascaded_handle in cascaded_facts {
184            if let Ok(fact_type) = self.working_memory
185                .get(&cascaded_handle)
186                .map(|f| f.fact_type.clone())
187                .ok_or_else(|| RuleEngineError::FieldNotFound {
188                    field: format!("FactHandle {} not found", cascaded_handle),
189                })
190            {
191                let _ = self.working_memory.retract(cascaded_handle);
192                // Propagate for each cascaded fact
193                self.propagate_changes_for_type(&fact_type);
194            }
195        }
196
197        // Trigger incremental propagation for this fact type
198        self.propagate_changes_for_type(&fact_type);
199
200        Ok(())
201    }
202    
203    /// Insert a fact with explicit assertion (user provided)
204    /// This fact will NOT be auto-retracted by TMS
205    pub fn insert_explicit(&mut self, fact_type: String, data: TypedFacts) -> FactHandle {
206        let handle = self.working_memory.insert(fact_type.clone(), data);
207        
208        // Add explicit justification in TMS
209        self.tms.add_explicit_justification(handle);
210        
211        // Trigger incremental propagation for this fact type
212        self.propagate_changes_for_type(&fact_type);
213        
214        handle
215    }
216    
217    /// Insert a fact with logical assertion (derived by a rule)
218    /// This fact WILL be auto-retracted if its premises become invalid
219    ///
220    /// # Arguments
221    /// * `fact_type` - Type of the fact (e.g., "Customer")
222    /// * `data` - The fact data
223    /// * `source_rule` - Name of the rule deriving this fact
224    /// * `premise_handles` - Handles of facts matched in the rule's WHEN clause
225    pub fn insert_logical(
226        &mut self,
227        fact_type: String,
228        data: TypedFacts,
229        source_rule: String,
230        premise_handles: Vec<FactHandle>,
231    ) -> FactHandle {
232        let handle = self.working_memory.insert(fact_type.clone(), data);
233        
234        // Add logical justification in TMS
235        self.tms.add_logical_justification(handle, source_rule, premise_handles);
236        
237        // Trigger incremental propagation for this fact type
238        self.propagate_changes_for_type(&fact_type);
239        
240        handle
241    }
242    
243    /// Get TMS reference
244    pub fn tms(&self) -> &TruthMaintenanceSystem {
245        &self.tms
246    }
247    
248    /// Get mutable TMS reference
249    pub fn tms_mut(&mut self) -> &mut TruthMaintenanceSystem {
250        &mut self.tms
251    }
252
253    /// Propagate changes for a specific fact type (incremental!)
254    fn propagate_changes_for_type(&mut self, fact_type: &str) {
255        // Get affected rules
256        let affected_rules = self.dependencies.get_affected_rules(fact_type);
257
258        if affected_rules.is_empty() {
259            return; // No rules depend on this fact type
260        }
261
262        // Get facts of this type
263        let facts_of_type = self.working_memory.get_by_type(fact_type);
264        
265        // Re-evaluate only affected rules, checking each fact individually
266        for &rule_idx in &affected_rules {
267            let rule = &self.rules[rule_idx];
268
269            // Check each fact of this type against the rule
270            for fact in &facts_of_type {
271                // Create TypedFacts for just this fact
272                let mut single_fact_data = TypedFacts::new();
273                for (key, value) in fact.data.get_all() {
274                    single_fact_data.set(format!("{}.{}", fact_type, key), value.clone());
275                }
276                // Store handle for Retract action
277                single_fact_data.set_fact_handle(fact_type.to_string(), fact.handle);
278                
279                // Evaluate rule condition with this single fact
280                let matches = super::network::evaluate_rete_ul_node_typed(&rule.node, &single_fact_data);
281
282                if matches {
283                    // Create activation for this specific fact match
284                    let activation = Activation::new(rule.name.clone(), rule.priority)
285                        .with_no_loop(rule.no_loop)
286                        .with_matched_fact(fact.handle);
287
288                    self.agenda.add_activation(activation);
289                }
290            }
291        }
292    }
293
294    /// Propagate changes for all fact types (re-evaluate all rules)
295    fn propagate_changes(&mut self) {
296        // Get all fact types
297        let fact_types: Vec<String> = self.working_memory.get_all_facts()
298            .iter()
299            .map(|f| f.fact_type.clone())
300            .collect::<std::collections::HashSet<_>>()
301            .into_iter()
302            .collect();
303        
304        // Evaluate each fact type using per-fact evaluation
305        for fact_type in fact_types {
306            let facts_of_type = self.working_memory.get_by_type(&fact_type);
307            
308            for (rule_idx, rule) in self.rules.iter().enumerate() {
309                // Skip if rule has no-loop and already fired
310                if rule.no_loop && self.agenda.has_fired(&rule.name) {
311                    continue;
312                }
313                
314                // Check each fact against the rule
315                for fact in &facts_of_type {
316                    let mut single_fact_data = TypedFacts::new();
317                    for (key, value) in fact.data.get_all() {
318                        single_fact_data.set(format!("{}.{}", fact_type, key), value.clone());
319                    }
320                    
321                    let matches = super::network::evaluate_rete_ul_node_typed(&rule.node, &single_fact_data);
322                    
323                    if matches {
324                        let activation = Activation::new(rule.name.clone(), rule.priority)
325                            .with_no_loop(rule.no_loop)
326                            .with_matched_fact(fact.handle);
327                        
328                        self.agenda.add_activation(activation);
329                    }
330                }
331            }
332        }
333    }
334
335    /// Fire all pending activations
336    pub fn fire_all(&mut self) -> Vec<String> {
337        let mut fired_rules = Vec::new();
338        let max_iterations = 1000; // Prevent infinite loops
339        let mut iteration_count = 0;
340
341        while let Some(activation) = self.agenda.get_next_activation() {
342            iteration_count += 1;
343            if iteration_count > max_iterations {
344                eprintln!("WARNING: Maximum iterations ({}) reached in fire_all(). Possible infinite loop!", max_iterations);
345                break;
346            }
347            
348            // Find rule
349            if let Some((idx, rule)) = self.rules
350                .iter_mut()
351                .enumerate()
352                .find(|(_, r)| r.name == activation.rule_name)
353            {
354                // Validate that matched fact still exists (hasn't been retracted)
355                if let Some(matched_handle) = activation.matched_fact_handle {
356                    if self.working_memory.get(&matched_handle).is_none() {
357                        // Fact was retracted, skip this activation
358                        continue;
359                    }
360                }
361                
362                // Execute action on a copy of all facts
363                let original_facts = self.working_memory.to_typed_facts();
364                let mut modified_facts = original_facts.clone();
365                
366                // Inject matched fact handle if available
367                if let Some(matched_handle) = activation.matched_fact_handle {
368                    // Find the fact type from working memory
369                    if let Some(fact) = self.working_memory.get(&matched_handle) {
370                        modified_facts.set_fact_handle(fact.fact_type.clone(), matched_handle);
371                    }
372                }
373                
374                let mut action_results = super::ActionResults::new();
375                (rule.action)(&mut modified_facts, &mut action_results);
376
377                // Update working memory: detect changes and apply them
378                // Build a map of fact_type -> updated fields
379                let mut updates_by_type: HashMap<String, Vec<(String, FactValue)>> = HashMap::new();
380                
381                for (key, value) in modified_facts.get_all() {
382                    // Keys are in format "FactType.field" or "FactType.handle.field"
383                    // We want to extract the FactType and field name
384                    if let Some(original_value) = original_facts.get(key) {
385                        if original_value != value {
386                            // Value changed! Extract fact type and field
387                            let parts: Vec<&str> = key.split('.').collect();
388                            if parts.len() >= 2 {
389                                let fact_type = parts[0].to_string();
390                                // Field is the last part (skip handle if present)
391                                let field = if parts.len() == 2 {
392                                    parts[1].to_string()
393                                } else {
394                                    parts[parts.len() - 1].to_string()
395                                };
396                                
397                                updates_by_type
398                                    .entry(fact_type)
399                                    .or_insert_with(Vec::new)
400                                    .push((field, value.clone()));
401                            }
402                        }
403                    } else {
404                        // New field added
405                        let parts: Vec<&str> = key.split('.').collect();
406                        if parts.len() >= 2 {
407                            let fact_type = parts[0].to_string();
408                            let field = if parts.len() == 2 {
409                                parts[1].to_string()
410                            } else {
411                                parts[parts.len() - 1].to_string()
412                            };
413                            
414                            updates_by_type
415                                .entry(fact_type)
416                                .or_insert_with(Vec::new)
417                                .push((field, value.clone()));
418                        }
419                    }
420                }
421                
422                // Apply updates to working memory facts
423                for (fact_type, field_updates) in updates_by_type {
424                    // Get handles of all facts of this type (collect to avoid borrow issues)
425                    let fact_handles: Vec<FactHandle> = self.working_memory
426                        .get_by_type(&fact_type)
427                        .iter()
428                        .map(|f| f.handle)
429                        .collect();
430                    
431                    for handle in fact_handles {
432                        if let Some(fact) = self.working_memory.get(&handle) {
433                            let mut updated_data = fact.data.clone();
434                            
435                            // Apply all field updates
436                            for (field, value) in &field_updates {
437                                updated_data.set(field, value.clone());
438                            }
439                            
440                            let _ = self.working_memory.update(handle, updated_data);
441                        }
442                    }
443                }
444
445                // Re-evaluate matches after working memory update
446                // This allows subsequent rules to see the updated values
447                self.propagate_changes();
448
449                // Process action results (retractions, agenda activations, etc.)
450                self.process_action_results(action_results);
451
452                // Track fired rule
453                fired_rules.push(activation.rule_name.clone());
454                self.agenda.mark_rule_fired(&activation);
455            }
456        }
457
458        fired_rules
459    }
460
461    /// Process action results from rule execution
462    fn process_action_results(&mut self, results: super::ActionResults) {
463        for result in results.results {
464            match result {
465                super::ActionResult::Retract(handle) => {
466                    // Retract fact by handle
467                    if let Err(e) = self.retract(handle) {
468                        eprintln!("❌ Failed to retract fact {:?}: {}", handle, e);
469                    }
470                }
471                super::ActionResult::RetractByType(fact_type) => {
472                    // Retract first fact of this type
473                    let facts_of_type = self.working_memory.get_by_type(&fact_type);
474                    if let Some(fact) = facts_of_type.first() {
475                        let handle = fact.handle;
476                        if let Err(e) = self.retract(handle) {
477                            eprintln!("❌ Failed to retract fact {:?}: {}", handle, e);
478                        }
479                    }
480                }
481                super::ActionResult::Update(handle) => {
482                    // Re-evaluate rules that depend on this fact type
483                    if let Some(fact) = self.working_memory.get(&handle) {
484                        let fact_type = fact.fact_type.clone();
485                        self.propagate_changes_for_type(&fact_type);
486                    }
487                }
488                super::ActionResult::ActivateAgendaGroup(group) => {
489                    // Activate agenda group
490                    self.agenda.set_focus(group);
491                }
492                super::ActionResult::InsertFact { fact_type, data } => {
493                    // Insert new explicit fact
494                    self.insert_explicit(fact_type, data);
495                }
496                super::ActionResult::InsertLogicalFact { fact_type, data, rule_name, premises } => {
497                    // Insert new logical fact
498                    let _handle = self.insert_logical(fact_type, data, rule_name, premises);
499                }
500                super::ActionResult::CallFunction { function_name, args } => {
501                    // Log function calls (requires function registry to actually execute)
502                    println!("🔧 Function call queued: {}({:?})", function_name, args);
503                    // TODO: Implement function registry
504                }
505                super::ActionResult::ScheduleRule { rule_name, delay_ms } => {
506                    // Log scheduled rules (requires scheduler to actually execute)
507                    println!("⏰ Rule scheduled: {} after {}ms", rule_name, delay_ms);
508                    // TODO: Implement rule scheduler
509                }
510                super::ActionResult::None => {
511                    // No action needed
512                }
513            }
514        }
515    }
516
517    /// Get working memory
518    pub fn working_memory(&self) -> &WorkingMemory {
519        &self.working_memory
520    }
521
522    /// Get mutable working memory
523    pub fn working_memory_mut(&mut self) -> &mut WorkingMemory {
524        &mut self.working_memory
525    }
526
527    /// Get agenda
528    pub fn agenda(&self) -> &AdvancedAgenda {
529        &self.agenda
530    }
531
532    /// Get mutable agenda
533    pub fn agenda_mut(&mut self) -> &mut AdvancedAgenda {
534        &mut self.agenda
535    }
536
537    /// Set conflict resolution strategy
538    ///
539    /// Controls how conflicting rules in the agenda are ordered.
540    /// Available strategies: Salience (default), LEX, MEA, Depth, Breadth, Simplicity, Complexity, Random
541    pub fn set_conflict_resolution_strategy(
542        &mut self,
543        strategy: super::agenda::ConflictResolutionStrategy,
544    ) {
545        self.agenda.set_strategy(strategy);
546    }
547
548    /// Get current conflict resolution strategy
549    pub fn conflict_resolution_strategy(&self) -> super::agenda::ConflictResolutionStrategy {
550        self.agenda.strategy()
551    }
552
553    /// Get statistics
554    pub fn stats(&self) -> IncrementalEngineStats {
555        IncrementalEngineStats {
556            rules: self.rules.len(),
557            working_memory: self.working_memory.stats(),
558            agenda: self.agenda.stats(),
559            dependencies: self.dependencies.fact_type_to_rules.len(),
560        }
561    }
562
563    /// Clear fired flags and reset agenda
564    pub fn reset(&mut self) {
565        self.agenda.reset_fired_flags();
566    }
567
568    /// Get template registry
569    pub fn templates(&self) -> &TemplateRegistry {
570        &self.templates
571    }
572
573    /// Get mutable template registry
574    pub fn templates_mut(&mut self) -> &mut TemplateRegistry {
575        &mut self.templates
576    }
577
578    /// Register a custom function for Test CE support
579    ///
580    /// # Example
581    /// ```
582    /// use rust_rule_engine::rete::{IncrementalEngine, FactValue};
583    ///
584    /// let mut engine = IncrementalEngine::new();
585    /// engine.register_function(
586    ///     "is_valid_email",
587    ///     |args, _facts| {
588    ///         if let Some(FactValue::String(email)) = args.first() {
589    ///             Ok(FactValue::Boolean(email.contains('@')))
590    ///         } else {
591    ///             Ok(FactValue::Boolean(false))
592    ///         }
593    ///     }
594    /// );
595    /// ```
596    pub fn register_function<F>(&mut self, name: &str, func: F)
597    where
598        F: Fn(&[FactValue], &TypedFacts) -> Result<FactValue> + Send + Sync + 'static,
599    {
600        self.custom_functions.insert(name.to_string(), Arc::new(func));
601    }
602
603    /// Get a custom function by name (for Test CE evaluation)
604    pub fn get_function(&self, name: &str) -> Option<&ReteCustomFunction> {
605        self.custom_functions.get(name)
606    }
607
608    /// Get global variables registry
609    pub fn globals(&self) -> &GlobalsRegistry {
610        &self.globals
611    }
612
613    /// Get mutable global variables registry
614    pub fn globals_mut(&mut self) -> &mut GlobalsRegistry {
615        &mut self.globals
616    }
617
618    /// Get deffacts registry
619    pub fn deffacts(&self) -> &DeffactsRegistry {
620        &self.deffacts
621    }
622
623    /// Get mutable deffacts registry
624    pub fn deffacts_mut(&mut self) -> &mut DeffactsRegistry {
625        &mut self.deffacts
626    }
627
628    /// Load all registered deffacts into working memory
629    /// Returns handles of all inserted facts
630    pub fn load_deffacts(&mut self) -> Vec<FactHandle> {
631        let mut handles = Vec::new();
632
633        // Get all facts from all registered deffacts
634        let all_facts = self.deffacts.get_all_facts();
635
636        for (_deffacts_name, fact_instance) in all_facts {
637            // Check if template exists for this fact type
638            let handle = if self.templates.get(&fact_instance.fact_type).is_some() {
639                // Use template validation
640                match self.insert_with_template(&fact_instance.fact_type, fact_instance.data) {
641                    Ok(h) => h,
642                    Err(_) => continue, // Skip invalid facts
643                }
644            } else {
645                // Insert without template validation
646                self.insert(fact_instance.fact_type, fact_instance.data)
647            };
648
649            handles.push(handle);
650        }
651
652        handles
653    }
654
655    /// Load a specific deffacts set by name
656    /// Returns handles of inserted facts or error if deffacts not found
657    pub fn load_deffacts_by_name(&mut self, name: &str) -> crate::errors::Result<Vec<FactHandle>> {
658        // Clone the facts to avoid borrow checker issues
659        let facts_to_insert = {
660            let deffacts = self.deffacts.get(name).ok_or_else(|| {
661                crate::errors::RuleEngineError::EvaluationError {
662                    message: format!("Deffacts '{}' not found", name),
663                }
664            })?;
665            deffacts.facts.clone()
666        };
667
668        let mut handles = Vec::new();
669
670        for fact_instance in facts_to_insert {
671            // Check if template exists for this fact type
672            let handle = if self.templates.get(&fact_instance.fact_type).is_some() {
673                // Use template validation
674                self.insert_with_template(&fact_instance.fact_type, fact_instance.data)?
675            } else {
676                // Insert without template validation
677                self.insert(fact_instance.fact_type, fact_instance.data)
678            };
679
680            handles.push(handle);
681        }
682
683        Ok(handles)
684    }
685
686    /// Reset engine and reload all deffacts (similar to CLIPS reset)
687    /// Clears working memory and agenda, then loads all deffacts
688    pub fn reset_with_deffacts(&mut self) -> Vec<FactHandle> {
689        // Clear working memory and agenda
690        self.working_memory = WorkingMemory::new();
691        self.agenda.clear();
692        self.rule_matched_facts.clear();
693
694        // Reload all deffacts
695        self.load_deffacts()
696    }
697
698    /// Insert a typed fact with template validation
699    pub fn insert_with_template(
700        &mut self,
701        template_name: &str,
702        data: TypedFacts,
703    ) -> crate::errors::Result<FactHandle> {
704        // Validate against template
705        self.templates.validate(template_name, &data)?;
706
707        // Insert into working memory
708        Ok(self.insert(template_name.to_string(), data))
709    }
710}
711
712impl Default for IncrementalEngine {
713    fn default() -> Self {
714        Self::new()
715    }
716}
717
718/// Engine statistics
719#[derive(Debug)]
720pub struct IncrementalEngineStats {
721    pub rules: usize,
722    pub working_memory: super::working_memory::WorkingMemoryStats,
723    pub agenda: super::agenda::AgendaStats,
724    pub dependencies: usize,
725}
726
727impl std::fmt::Display for IncrementalEngineStats {
728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729        write!(
730            f,
731            "Engine Stats: {} rules, {} fact types tracked\nWM: {}\nAgenda: {}",
732            self.rules,
733            self.dependencies,
734            self.working_memory,
735            self.agenda
736        )
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use crate::rete::network::ReteUlNode;
744    use crate::rete::alpha::AlphaNode;
745
746    #[test]
747    fn test_dependency_graph() {
748        let mut graph = RuleDependencyGraph::new();
749
750        graph.add_dependency(0, "Person".to_string());
751        graph.add_dependency(1, "Person".to_string());
752        graph.add_dependency(1, "Order".to_string());
753
754        let affected = graph.get_affected_rules("Person");
755        assert_eq!(affected.len(), 2);
756        assert!(affected.contains(&0));
757        assert!(affected.contains(&1));
758
759        let deps = graph.get_rule_dependencies(1);
760        assert_eq!(deps.len(), 2);
761        assert!(deps.contains("Person"));
762        assert!(deps.contains("Order"));
763    }
764
765    #[test]
766    fn test_incremental_propagation() {
767        let mut engine = IncrementalEngine::new();
768
769        // Add rule that depends on "Person" type
770        let node = ReteUlNode::UlAlpha(AlphaNode {
771            field: "Person.age".to_string(),
772            operator: ">".to_string(),
773            value: "18".to_string(),
774        });
775
776        let rule = TypedReteUlRule {
777            name: "IsAdult".to_string(),
778            node,
779            priority: 0,
780            no_loop: true,
781            action: std::sync::Arc::new(|_, _| {}),
782        };
783
784        engine.add_rule(rule, vec!["Person".to_string()]);
785
786        // Insert Person fact
787        let mut person = TypedFacts::new();
788        person.set("age", 25i64);
789        let handle = engine.insert("Person".to_string(), person);
790
791        // Check that rule was activated
792        let stats = engine.stats();
793        assert!(stats.agenda.total_activations > 0);
794
795        // Update person
796        let mut updated = TypedFacts::new();
797        updated.set("age", 15i64); // Now under 18
798        engine.update(handle, updated).unwrap();
799
800        // Rule should be re-evaluated (incrementally)
801    }
802}