ricecoder_learning/
conflict_resolver.rs

1/// Conflict detection and resolution for rules
2use crate::error::{LearningError, Result};
3use crate::models::{Rule, RuleScope};
4use std::collections::HashMap;
5
6/// Detects and resolves conflicts between rules
7pub struct ConflictResolver;
8
9impl ConflictResolver {
10    /// Create a new conflict resolver
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Detect if two rules conflict
16    pub fn detect_conflict(rule1: &Rule, rule2: &Rule) -> bool {
17        // Rules conflict if they have the same pattern but different actions
18        rule1.pattern == rule2.pattern && rule1.action != rule2.action
19    }
20
21    /// Find all conflicts in a set of rules
22    pub fn find_conflicts(rules: &[Rule]) -> Vec<(Rule, Rule)> {
23        let mut conflicts = Vec::new();
24
25        for i in 0..rules.len() {
26            for j in (i + 1)..rules.len() {
27                if Self::detect_conflict(&rules[i], &rules[j]) {
28                    conflicts.push((rules[i].clone(), rules[j].clone()));
29                }
30            }
31        }
32
33        conflicts
34    }
35
36    /// Check if a rule conflicts with existing rules
37    pub fn check_conflicts(rule: &Rule, existing_rules: &[Rule]) -> Result<()> {
38        for existing_rule in existing_rules {
39            if Self::detect_conflict(rule, existing_rule) {
40                return Err(LearningError::ConflictResolutionFailed(
41                    format!(
42                        "Rule '{}' conflicts with existing rule '{}': both match pattern '{}' but have different actions",
43                        rule.id, existing_rule.id, rule.pattern
44                    ),
45                ));
46            }
47        }
48        Ok(())
49    }
50
51    /// Apply scope precedence to select the appropriate rule
52    /// Project rules override global rules when both exist
53    pub fn apply_precedence(rules: &[Rule]) -> Option<Rule> {
54        if rules.is_empty() {
55            return None;
56        }
57
58        // Sort by scope precedence: Project > Global > Session
59        let mut sorted_rules = rules.to_vec();
60        sorted_rules.sort_by_key(|r| match r.scope {
61            RuleScope::Project => 0,
62            RuleScope::Global => 1,
63            RuleScope::Session => 2,
64        });
65
66        Some(sorted_rules[0].clone())
67    }
68
69    /// Get rules by pattern, applying scope precedence
70    pub fn get_rules_by_pattern_with_precedence(
71        rules: &[Rule],
72        pattern: &str,
73    ) -> Vec<Rule> {
74        let matching_rules: Vec<Rule> = rules
75            .iter()
76            .filter(|r| r.pattern == pattern)
77            .cloned()
78            .collect();
79
80        if matching_rules.is_empty() {
81            return Vec::new();
82        }
83
84        // Group by pattern and apply precedence
85        let mut result = Vec::new();
86        let mut seen_patterns = std::collections::HashSet::new();
87
88        for rule in &matching_rules {
89            if !seen_patterns.contains(&rule.pattern) {
90                if let Some(precedent_rule) = Self::apply_precedence(
91                    &matching_rules
92                        .iter()
93                        .filter(|r| r.pattern == rule.pattern)
94                        .cloned()
95                        .collect::<Vec<_>>(),
96                ) {
97                    result.push(precedent_rule);
98                    seen_patterns.insert(rule.pattern.clone());
99                }
100            }
101        }
102
103        result
104    }
105
106    /// Resolve conflicts by applying scope precedence
107    pub fn resolve_conflicts(rules: &[Rule]) -> Result<Vec<Rule>> {
108        // Group rules by pattern
109        let mut pattern_groups: HashMap<String, Vec<Rule>> = HashMap::new();
110
111        for rule in rules {
112            pattern_groups
113                .entry(rule.pattern.clone())
114                .or_insert_with(Vec::new)
115                .push(rule.clone());
116        }
117
118        // For each pattern group, apply precedence
119        let mut resolved_rules = Vec::new();
120
121        for (_, group) in pattern_groups {
122            if let Some(rule) = Self::apply_precedence(&group) {
123                resolved_rules.push(rule);
124            }
125        }
126
127        Ok(resolved_rules)
128    }
129
130    /// Log conflict resolution decision
131    pub fn log_conflict_resolution(
132        selected_rule: &Rule,
133        conflicting_rules: &[Rule],
134    ) -> String {
135        let conflicting_ids: Vec<String> = conflicting_rules
136            .iter()
137            .map(|r| r.id.clone())
138            .collect();
139
140        format!(
141            "Conflict resolution: Selected rule '{}' (scope: {}) over conflicting rules: {}",
142            selected_rule.id,
143            selected_rule.scope,
144            conflicting_ids.join(", ")
145        )
146    }
147
148    /// Get the highest priority rule for a pattern across all scopes
149    pub fn get_highest_priority_rule(rules: &[Rule], pattern: &str) -> Option<Rule> {
150        let matching_rules: Vec<Rule> = rules
151            .iter()
152            .filter(|r| r.pattern == pattern)
153            .cloned()
154            .collect();
155
156        Self::apply_precedence(&matching_rules)
157    }
158
159    /// Check if rules in different scopes conflict
160    pub fn check_cross_scope_conflicts(
161        project_rules: &[Rule],
162        global_rules: &[Rule],
163    ) -> Vec<(Rule, Rule)> {
164        let mut conflicts = Vec::new();
165
166        for project_rule in project_rules {
167            for global_rule in global_rules {
168                if Self::detect_conflict(project_rule, global_rule) {
169                    conflicts.push((project_rule.clone(), global_rule.clone()));
170                }
171            }
172        }
173
174        conflicts
175    }
176}
177
178impl Default for ConflictResolver {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::models::RuleSource;
188
189    fn create_test_rule(
190        id: &str,
191        scope: RuleScope,
192        pattern: &str,
193        action: &str,
194    ) -> Rule {
195        let mut rule = Rule::new(
196            scope,
197            pattern.to_string(),
198            action.to_string(),
199            RuleSource::Learned,
200        );
201        rule.id = id.to_string();
202        rule
203    }
204
205    #[test]
206    fn test_detect_conflict_same_pattern_different_action() {
207        let rule1 = create_test_rule("rule1", RuleScope::Global, "pattern1", "action1");
208        let rule2 = create_test_rule("rule2", RuleScope::Global, "pattern1", "action2");
209
210        assert!(ConflictResolver::detect_conflict(&rule1, &rule2));
211    }
212
213    #[test]
214    fn test_detect_conflict_same_pattern_same_action() {
215        let rule1 = create_test_rule("rule1", RuleScope::Global, "pattern1", "action1");
216        let rule2 = create_test_rule("rule2", RuleScope::Global, "pattern1", "action1");
217
218        assert!(!ConflictResolver::detect_conflict(&rule1, &rule2));
219    }
220
221    #[test]
222    fn test_detect_conflict_different_pattern() {
223        let rule1 = create_test_rule("rule1", RuleScope::Global, "pattern1", "action1");
224        let rule2 = create_test_rule("rule2", RuleScope::Global, "pattern2", "action1");
225
226        assert!(!ConflictResolver::detect_conflict(&rule1, &rule2));
227    }
228
229    #[test]
230    fn test_find_conflicts() {
231        let rules = vec![
232            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
233            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
234            create_test_rule("rule3", RuleScope::Global, "pattern2", "action1"),
235        ];
236
237        let conflicts = ConflictResolver::find_conflicts(&rules);
238        assert_eq!(conflicts.len(), 1);
239        assert_eq!(conflicts[0].0.id, "rule1");
240        assert_eq!(conflicts[0].1.id, "rule2");
241    }
242
243    #[test]
244    fn test_check_conflicts_no_conflict() {
245        let rule = create_test_rule("rule1", RuleScope::Global, "pattern1", "action1");
246        let existing_rules = vec![
247            create_test_rule("rule2", RuleScope::Global, "pattern2", "action1"),
248            create_test_rule("rule3", RuleScope::Global, "pattern3", "action2"),
249        ];
250
251        let result = ConflictResolver::check_conflicts(&rule, &existing_rules);
252        assert!(result.is_ok());
253    }
254
255    #[test]
256    fn test_check_conflicts_with_conflict() {
257        let rule = create_test_rule("rule1", RuleScope::Global, "pattern1", "action1");
258        let existing_rules = vec![
259            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
260        ];
261
262        let result = ConflictResolver::check_conflicts(&rule, &existing_rules);
263        assert!(result.is_err());
264    }
265
266    #[test]
267    fn test_apply_precedence_project_over_global() {
268        let rules = vec![
269            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
270            create_test_rule("rule2", RuleScope::Project, "pattern1", "action2"),
271        ];
272
273        let selected = ConflictResolver::apply_precedence(&rules);
274        assert!(selected.is_some());
275        assert_eq!(selected.unwrap().id, "rule2");
276    }
277
278    #[test]
279    fn test_apply_precedence_global_over_session() {
280        let rules = vec![
281            create_test_rule("rule1", RuleScope::Session, "pattern1", "action1"),
282            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
283        ];
284
285        let selected = ConflictResolver::apply_precedence(&rules);
286        assert!(selected.is_some());
287        assert_eq!(selected.unwrap().id, "rule2");
288    }
289
290    #[test]
291    fn test_apply_precedence_project_over_all() {
292        let rules = vec![
293            create_test_rule("rule1", RuleScope::Session, "pattern1", "action1"),
294            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
295            create_test_rule("rule3", RuleScope::Project, "pattern1", "action3"),
296        ];
297
298        let selected = ConflictResolver::apply_precedence(&rules);
299        assert!(selected.is_some());
300        assert_eq!(selected.unwrap().id, "rule3");
301    }
302
303    #[test]
304    fn test_apply_precedence_empty() {
305        let rules = vec![];
306        let selected = ConflictResolver::apply_precedence(&rules);
307        assert!(selected.is_none());
308    }
309
310    #[test]
311    fn test_resolve_conflicts_multiple_patterns() {
312        let rules = vec![
313            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
314            create_test_rule("rule2", RuleScope::Project, "pattern1", "action2"),
315            create_test_rule("rule3", RuleScope::Global, "pattern2", "action1"),
316            create_test_rule("rule4", RuleScope::Session, "pattern2", "action2"),
317        ];
318
319        let resolved = ConflictResolver::resolve_conflicts(&rules).unwrap();
320        assert_eq!(resolved.len(), 2);
321
322        // Check that project rule is selected for pattern1
323        let pattern1_rule = resolved.iter().find(|r| r.pattern == "pattern1").unwrap();
324        assert_eq!(pattern1_rule.id, "rule2");
325
326        // Check that global rule is selected for pattern2
327        let pattern2_rule = resolved.iter().find(|r| r.pattern == "pattern2").unwrap();
328        assert_eq!(pattern2_rule.id, "rule3");
329    }
330
331    #[test]
332    fn test_log_conflict_resolution() {
333        let selected = create_test_rule("rule1", RuleScope::Project, "pattern1", "action1");
334        let conflicting = vec![
335            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
336        ];
337
338        let log = ConflictResolver::log_conflict_resolution(&selected, &conflicting);
339        assert!(log.contains("rule1"));
340        assert!(log.contains("project"));
341        assert!(log.contains("rule2"));
342    }
343
344    #[test]
345    fn test_get_highest_priority_rule() {
346        let rules = vec![
347            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
348            create_test_rule("rule2", RuleScope::Project, "pattern1", "action2"),
349            create_test_rule("rule3", RuleScope::Session, "pattern1", "action3"),
350        ];
351
352        let highest = ConflictResolver::get_highest_priority_rule(&rules, "pattern1");
353        assert!(highest.is_some());
354        assert_eq!(highest.unwrap().id, "rule2");
355    }
356
357    #[test]
358    fn test_get_highest_priority_rule_not_found() {
359        let rules = vec![
360            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
361        ];
362
363        let highest = ConflictResolver::get_highest_priority_rule(&rules, "pattern2");
364        assert!(highest.is_none());
365    }
366
367    #[test]
368    fn test_check_cross_scope_conflicts() {
369        let project_rules = vec![
370            create_test_rule("rule1", RuleScope::Project, "pattern1", "action1"),
371        ];
372
373        let global_rules = vec![
374            create_test_rule("rule2", RuleScope::Global, "pattern1", "action2"),
375        ];
376
377        let conflicts = ConflictResolver::check_cross_scope_conflicts(&project_rules, &global_rules);
378        assert_eq!(conflicts.len(), 1);
379    }
380
381    #[test]
382    fn test_check_cross_scope_no_conflicts() {
383        let project_rules = vec![
384            create_test_rule("rule1", RuleScope::Project, "pattern1", "action1"),
385        ];
386
387        let global_rules = vec![
388            create_test_rule("rule2", RuleScope::Global, "pattern2", "action1"),
389        ];
390
391        let conflicts = ConflictResolver::check_cross_scope_conflicts(&project_rules, &global_rules);
392        assert_eq!(conflicts.len(), 0);
393    }
394
395    #[test]
396    fn test_get_rules_by_pattern_with_precedence() {
397        let rules = vec![
398            create_test_rule("rule1", RuleScope::Global, "pattern1", "action1"),
399            create_test_rule("rule2", RuleScope::Project, "pattern1", "action2"),
400            create_test_rule("rule3", RuleScope::Session, "pattern1", "action3"),
401            create_test_rule("rule4", RuleScope::Global, "pattern2", "action1"),
402        ];
403
404        let pattern1_rules = ConflictResolver::get_rules_by_pattern_with_precedence(&rules, "pattern1");
405        assert_eq!(pattern1_rules.len(), 1);
406        assert_eq!(pattern1_rules[0].id, "rule2");
407
408        let pattern2_rules = ConflictResolver::get_rules_by_pattern_with_precedence(&rules, "pattern2");
409        assert_eq!(pattern2_rules.len(), 1);
410        assert_eq!(pattern2_rules[0].id, "rule4");
411    }
412}