Skip to main content

ipfrs_tensorlogic/
rule_conflict_resolver.rs

1//! RuleConflictResolver — production-quality logic rule conflict detection and resolution.
2//!
3//! This module detects five categories of conflict among [`LogicRule`]s and resolves
4//! them via pluggable [`ResolutionStrategy`] implementations.
5//!
6//! # Conflict categories
7//!
8//! | Variant | Meaning |
9//! |---------|---------|
10//! | [`ConflictType::DirectContradiction`] | Same head, disjoint bodies. |
11//! | [`ConflictType::PriorityConflict`] | Same head, different priorities. |
12//! | [`ConflictType::CyclicDependency`] | Premise→head graph contains a back-edge. |
13//! | [`ConflictType::UndercutConflict`] | One rule's head negates another rule. |
14//! | [`ConflictType::RebuttalConflict`] | Two rules have complementary heads. |
15//!
16//! # Resolution strategies
17//!
18//! | Variant | Meaning |
19//! |---------|---------|
20//! | [`ResolutionStrategy::PriorityOrder`] | Higher `priority` wins. |
21//! | [`ResolutionStrategy::Specificity`] | More body conditions wins. |
22//! | [`ResolutionStrategy::LastWriter`] | More recently added rule wins. |
23//! | [`ResolutionStrategy::Inhibit`] | Defeasible rule loses; tie → higher priority. |
24//! | [`ResolutionStrategy::Merge`] | Named merge operator (future extension). |
25//! | [`ResolutionStrategy::AskOracle`] | Returns [`ResolverError::UnresolvableConflict`]. |
26//!
27//! # Examples
28//!
29//! ```
30//! use ipfrs_tensorlogic::rule_conflict_resolver::{
31//!     LogicRule, ResolverConfig, ResolutionStrategy, RuleConflictResolver,
32//! };
33//!
34//! let cfg = ResolverConfig::default();
35//! let mut resolver = RuleConflictResolver::new(cfg);
36//!
37//! resolver.add_rule(LogicRule {
38//!     id: "r1".to_string(),
39//!     head: "bird".to_string(),
40//!     body: vec!["has_wings".to_string(), "feathers".to_string()],
41//!     priority: 10,
42//!     source: "ornithology".to_string(),
43//!     is_defeasible: false,
44//! }).expect("test setup: add_rule should not fail");
45//!
46//! resolver.add_rule(LogicRule {
47//!     id: "r2".to_string(),
48//!     head: "bird".to_string(),
49//!     body: vec!["lays_eggs".to_string()],
50//!     priority: 5,
51//!     source: "biology".to_string(),
52//!     is_defeasible: true,
53//! }).expect("test setup: add_rule should not fail");
54//!
55//! let conflicts = resolver.detect_conflicts();
56//! assert!(!conflicts.is_empty());
57//!
58//! let stats = resolver.stats();
59//! assert_eq!(stats.rules_loaded, 2);
60//! ```
61
62use std::collections::{HashMap, HashSet, VecDeque};
63
64// ─── xorshift64 (no-rand policy) ──────────────────────────────────────────────
65
66/// Fast 64-bit pseudo-random number generator (xorshift64 algorithm).
67/// Used only in tests; exported with crate-root alias `rcr_xorshift64`.
68#[inline]
69pub fn xorshift64(state: &mut u64) -> u64 {
70    let mut x = *state;
71    x ^= x << 13;
72    x ^= x >> 7;
73    x ^= x << 17;
74    *state = x;
75    x
76}
77
78// ─── Core types ───────────────────────────────────────────────────────────────
79
80/// A single logic rule with metadata for conflict analysis.
81#[derive(Debug, Clone, PartialEq)]
82pub struct LogicRule {
83    /// Unique identifier for this rule.
84    pub id: String,
85    /// The conclusion (head) of the rule, e.g. `"bird"`.
86    pub head: String,
87    /// Premises (body) of the rule.  A premise may carry a `"NOT:"` prefix
88    /// to represent negation-as-failure.
89    pub body: Vec<String>,
90    /// Numeric priority; higher values take precedence.
91    pub priority: i32,
92    /// Provenance / source identifier.
93    pub source: String,
94    /// Whether this rule can be defeated by a higher-priority rule.
95    pub is_defeasible: bool,
96}
97
98impl LogicRule {
99    /// Return the positive (non-negated) premises in `body`.
100    #[inline]
101    pub fn positive_body(&self) -> impl Iterator<Item = &str> {
102        self.body
103            .iter()
104            .filter(|p| !p.starts_with("NOT:"))
105            .map(String::as_str)
106    }
107
108    /// Return the negated conditions (with the `"NOT:"` prefix stripped).
109    #[inline]
110    pub fn negated_body(&self) -> impl Iterator<Item = &str> {
111        self.body
112            .iter()
113            .filter(|p| p.starts_with("NOT:"))
114            .map(|p| &p["NOT:".len()..])
115    }
116}
117
118// ─── ConflictType ─────────────────────────────────────────────────────────────
119
120/// The category of a detected conflict between two or more rules.
121#[derive(Debug, Clone, PartialEq)]
122pub enum ConflictType {
123    /// Two rules derive the same head with completely disjoint bodies.
124    DirectContradiction {
125        /// ID of the first conflicting rule.
126        rule_a: String,
127        /// ID of the second conflicting rule.
128        rule_b: String,
129    },
130    /// Two rules derive the same head but with different priority values.
131    PriorityConflict {
132        /// ID of the higher-priority rule.
133        higher: String,
134        /// ID of the lower-priority rule.
135        lower: String,
136    },
137    /// A dependency cycle was found in the rule graph.
138    CyclicDependency {
139        /// Ordered list of rule IDs forming the cycle.
140        cycle: Vec<String>,
141    },
142    /// Rule A's head appears as a negated premise in rule B, meaning A undercuts B.
143    UndercutConflict {
144        /// ID of the rule whose head undercuts the other.
145        undercutter: String,
146        /// ID of the rule being undercut.
147        undercut: String,
148    },
149    /// Rule A and rule B have complementary heads (`"NOT:<other_head>"`).
150    RebuttalConflict {
151        /// ID of the first rebutting rule.
152        rule_a: String,
153        /// ID of the second rebutting rule.
154        rule_b: String,
155    },
156}
157
158// ─── ResolutionStrategy ───────────────────────────────────────────────────────
159
160/// How a detected conflict should be resolved.
161#[derive(Debug, Clone, PartialEq)]
162pub enum ResolutionStrategy {
163    /// Higher `priority` field wins.
164    PriorityOrder,
165    /// Rule with more body conditions wins (more specific).
166    Specificity,
167    /// Rule added most recently (highest insertion index) wins.
168    LastWriter,
169    /// If a rule is defeasible, it loses; otherwise use priority.
170    Inhibit,
171    /// Apply a named merge operator (future extension).
172    Merge(String),
173    /// External oracle must decide; resolving returns an error.
174    AskOracle,
175}
176
177// ─── ConflictRecord ───────────────────────────────────────────────────────────
178
179/// A recorded conflict together with its (optional) resolution.
180#[derive(Debug, Clone)]
181pub struct ConflictRecord {
182    /// The kind and participating rules of the conflict.
183    pub conflict_type: ConflictType,
184    /// Unix-epoch milliseconds when this conflict was detected (monotonic counter
185    /// when a real clock is unavailable).
186    pub detected_at: u64,
187    /// Whether this conflict has been resolved.
188    pub resolved: bool,
189    /// The strategy that was applied (if resolved).
190    pub resolution: Option<ResolutionStrategy>,
191    /// The ID of the winning rule (if resolved and applicable).
192    pub winning_rule: Option<String>,
193}
194
195impl ConflictRecord {
196    fn new(conflict_type: ConflictType, timestamp: u64) -> Self {
197        Self {
198            conflict_type,
199            detected_at: timestamp,
200            resolved: false,
201            resolution: None,
202            winning_rule: None,
203        }
204    }
205}
206
207// ─── ResolverConfig ───────────────────────────────────────────────────────────
208
209/// Configuration for [`RuleConflictResolver`].
210#[derive(Debug, Clone)]
211pub struct ResolverConfig {
212    /// Strategy used by [`RuleConflictResolver::winning_rule`] and
213    /// [`RuleConflictResolver::resolve_all`] when no explicit strategy is given.
214    pub default_strategy: ResolutionStrategy,
215    /// Whether to run cycle detection during [`RuleConflictResolver::detect_conflicts`].
216    pub enable_cycle_detection: bool,
217    /// Hard cap on the number of rules that may be loaded simultaneously.
218    pub max_rules: usize,
219    /// Weight given to specificity when computing a composite score (reserved for
220    /// future weighted-strategy extensions).
221    pub specificity_weight: f64,
222    /// Weight given to priority (reserved for future weighted-strategy extensions).
223    pub priority_weight: f64,
224}
225
226impl Default for ResolverConfig {
227    fn default() -> Self {
228        Self {
229            default_strategy: ResolutionStrategy::PriorityOrder,
230            enable_cycle_detection: true,
231            max_rules: 10_000,
232            specificity_weight: 0.5,
233            priority_weight: 0.5,
234        }
235    }
236}
237
238// ─── ResolverStats ────────────────────────────────────────────────────────────
239
240/// Snapshot of resolver metrics.
241#[derive(Debug, Clone, Default)]
242pub struct ResolverStats {
243    /// Total rules currently loaded.
244    pub rules_loaded: usize,
245    /// Total conflicts ever detected (cumulative).
246    pub conflicts_detected: usize,
247    /// Total conflicts that have been resolved (cumulative).
248    pub conflicts_resolved: usize,
249    /// Total dependency cycles ever found.
250    pub cycles_found: usize,
251    /// Conflicts that have been detected but not yet resolved.
252    pub unresolved_conflicts: usize,
253}
254
255// ─── ResolverError ────────────────────────────────────────────────────────────
256
257/// Errors that can be returned by [`RuleConflictResolver`] operations.
258#[derive(Debug, Clone, PartialEq, thiserror::Error)]
259pub enum ResolverError {
260    /// A rule ID was referenced but no such rule exists.
261    #[error("rule not found: {0}")]
262    RuleNotFound(String),
263
264    /// The conflict between `rule_a` and `rule_b` cannot be resolved automatically.
265    #[error("unresolvable conflict between '{rule_a}' and '{rule_b}'")]
266    UnresolvableConflict {
267        /// First conflicting rule ID.
268        rule_a: String,
269        /// Second conflicting rule ID.
270        rule_b: String,
271    },
272
273    /// A cyclic dependency was detected among the listed rules.
274    #[error("cyclic dependency: {0:?}")]
275    CyclicDependency(Vec<String>),
276
277    /// Invalid or inconsistent resolver configuration.
278    #[error("configuration error: {0}")]
279    ConfigurationError(String),
280
281    /// The rule set would exceed [`ResolverConfig::max_rules`].
282    #[error("max rules exceeded")]
283    MaxRulesExceeded,
284}
285
286// ─── Internal helper ──────────────────────────────────────────────────────────
287
288/// Internal bookkeeping entry for a stored rule.
289#[derive(Debug, Clone)]
290struct RuleEntry {
291    rule: LogicRule,
292    /// Insertion index; used by the LastWriter strategy.
293    insertion_order: usize,
294}
295
296// ─── RuleConflictResolver ─────────────────────────────────────────────────────
297
298/// Production-quality conflict detector and resolver for logic rule sets.
299///
300/// See the [module-level documentation](self) for a full description.
301pub struct RuleConflictResolver {
302    /// Ordered storage; the key is the rule ID.
303    rules: HashMap<String, RuleEntry>,
304    /// Monotonically increasing counter; simulates a clock for `detected_at`.
305    clock: u64,
306    /// Configuration.
307    config: ResolverConfig,
308    /// Cumulative statistics.
309    stats: ResolverStats,
310}
311
312impl RuleConflictResolver {
313    /// Create a new resolver with the given configuration.
314    pub fn new(config: ResolverConfig) -> Self {
315        Self {
316            rules: HashMap::new(),
317            clock: 0,
318            config,
319            stats: ResolverStats::default(),
320        }
321    }
322
323    /// Advance the internal clock by one tick and return the new value.
324    #[inline]
325    fn tick(&mut self) -> u64 {
326        self.clock += 1;
327        self.clock
328    }
329
330    // ── Rule management ───────────────────────────────────────────────────────
331
332    /// Add a rule to the resolver.
333    ///
334    /// An immediate check for [`ConflictType::DirectContradiction`] with every
335    /// existing rule that shares the same head is performed.  The statistics are
336    /// updated but the conflicts are not automatically resolved.
337    ///
338    /// # Errors
339    ///
340    /// * [`ResolverError::MaxRulesExceeded`] when the hard cap would be breached.
341    pub fn add_rule(&mut self, rule: LogicRule) -> Result<(), ResolverError> {
342        if self.rules.len() >= self.config.max_rules {
343            return Err(ResolverError::MaxRulesExceeded);
344        }
345
346        let insertion_order = self.rules.len();
347        let ts = self.tick();
348
349        // Immediate contradiction check against all existing same-head rules.
350        let same_head: Vec<String> = self
351            .rules
352            .values()
353            .filter(|e| e.rule.head == rule.head)
354            .map(|e| e.rule.id.clone())
355            .collect();
356
357        for existing_id in same_head {
358            let existing_body: HashSet<&str> = self.rules[&existing_id]
359                .rule
360                .body
361                .iter()
362                .map(String::as_str)
363                .collect();
364            let new_body: HashSet<&str> = rule.body.iter().map(String::as_str).collect();
365
366            if existing_body.is_disjoint(&new_body) {
367                self.stats.conflicts_detected += 1;
368                let _ = ts; // timestamp consumed above
369            }
370        }
371
372        self.rules.insert(
373            rule.id.clone(),
374            RuleEntry {
375                rule,
376                insertion_order,
377            },
378        );
379
380        self.stats.rules_loaded = self.rules.len();
381        Ok(())
382    }
383
384    /// Remove a rule by ID.
385    ///
386    /// # Errors
387    ///
388    /// * [`ResolverError::RuleNotFound`] when no such rule exists.
389    pub fn remove_rule(&mut self, id: &str) -> Result<(), ResolverError> {
390        if self.rules.remove(id).is_none() {
391            return Err(ResolverError::RuleNotFound(id.to_string()));
392        }
393        self.stats.rules_loaded = self.rules.len();
394        Ok(())
395    }
396
397    // ── Conflict detection ────────────────────────────────────────────────────
398
399    /// Run a full conflict scan and return all detected [`ConflictRecord`]s.
400    ///
401    /// Detection order:
402    /// 1. [`ConflictType::DirectContradiction`]
403    /// 2. [`ConflictType::PriorityConflict`]
404    /// 3. [`ConflictType::CyclicDependency`] (if enabled in config)
405    /// 4. [`ConflictType::UndercutConflict`]
406    /// 5. [`ConflictType::RebuttalConflict`]
407    pub fn detect_conflicts(&mut self) -> Vec<ConflictRecord> {
408        // Collect clones so we don't hold a borrow on `self.rules` while
409        // calling `self.tick()` (which needs `&mut self`).
410        let rule_snapshots: Vec<LogicRule> = self.rules.values().map(|e| e.rule.clone()).collect();
411
412        let mut pending_types: Vec<ConflictType> = Vec::new();
413
414        // ── 1. DirectContradiction & 2. PriorityConflict ─────────────────────
415        for i in 0..rule_snapshots.len() {
416            for j in (i + 1)..rule_snapshots.len() {
417                let a = &rule_snapshots[i];
418                let b = &rule_snapshots[j];
419
420                if a.head != b.head {
421                    continue;
422                }
423
424                let body_a: HashSet<&str> = a.body.iter().map(String::as_str).collect();
425                let body_b: HashSet<&str> = b.body.iter().map(String::as_str).collect();
426
427                // DirectContradiction: same head, completely disjoint bodies.
428                if body_a.is_disjoint(&body_b) {
429                    pending_types.push(ConflictType::DirectContradiction {
430                        rule_a: a.id.clone(),
431                        rule_b: b.id.clone(),
432                    });
433                }
434
435                // PriorityConflict: same head, different priorities.
436                if a.priority != b.priority {
437                    let (higher, lower) = if a.priority > b.priority {
438                        (a.id.clone(), b.id.clone())
439                    } else {
440                        (b.id.clone(), a.id.clone())
441                    };
442                    pending_types.push(ConflictType::PriorityConflict { higher, lower });
443                }
444            }
445        }
446
447        // ── 3. CyclicDependency ───────────────────────────────────────────────
448        if self.config.enable_cycle_detection {
449            let cycles = self.detect_cycles();
450            let cycle_count = cycles.len();
451            for cycle in cycles {
452                pending_types.push(ConflictType::CyclicDependency { cycle });
453            }
454            self.stats.cycles_found += cycle_count;
455        }
456
457        // ── 4. UndercutConflict ───────────────────────────────────────────────
458        // Rule A undercuts rule B when A's head appears as "NOT:<head_A>" in B's body.
459        for a in &rule_snapshots {
460            let negated_head = format!("NOT:{}", a.head);
461            for b in &rule_snapshots {
462                if a.id == b.id {
463                    continue;
464                }
465                if b.body.contains(&negated_head) {
466                    pending_types.push(ConflictType::UndercutConflict {
467                        undercutter: a.id.clone(),
468                        undercut: b.id.clone(),
469                    });
470                }
471            }
472        }
473
474        // ── 5. RebuttalConflict ───────────────────────────────────────────────
475        // Rules A and B rebut each other when one head is "NOT:" + the other head.
476        for i in 0..rule_snapshots.len() {
477            for j in (i + 1)..rule_snapshots.len() {
478                let a = &rule_snapshots[i];
479                let b = &rule_snapshots[j];
480
481                let a_negates_b = a.head == format!("NOT:{}", b.head);
482                let b_negates_a = b.head == format!("NOT:{}", a.head);
483
484                if a_negates_b || b_negates_a {
485                    pending_types.push(ConflictType::RebuttalConflict {
486                        rule_a: a.id.clone(),
487                        rule_b: b.id.clone(),
488                    });
489                }
490            }
491        }
492
493        // Now mint timestamps and build ConflictRecords.
494        let records: Vec<ConflictRecord> = pending_types
495            .into_iter()
496            .map(|ct| {
497                let ts = self.tick();
498                ConflictRecord::new(ct, ts)
499            })
500            .collect();
501
502        // Update stats.
503        let new_conflicts = records.len();
504        self.stats.conflicts_detected += new_conflicts;
505        self.stats.unresolved_conflicts += new_conflicts;
506
507        records
508    }
509
510    /// DFS-based cycle detection on the premise→head dependency graph.
511    ///
512    /// Each rule contributes edges: `(premise_head → rule_head)` for every
513    /// positive premise that also appears as some rule's head.
514    fn detect_cycles(&self) -> Vec<Vec<String>> {
515        // Build adjacency list: head → heads that depend on it.
516        // An edge head_X → head_Y means "Y's body contains X" i.e. Y depends on X.
517        // We model it as: for each rule with head H and positive body premise P,
518        // add edge P → H.  A cycle in this graph means mutual dependency.
519
520        // Collect all known heads.
521        let all_heads: HashSet<&str> = self.rules.values().map(|e| e.rule.head.as_str()).collect();
522
523        // Build adjacency: premise → {conclusions that depend on it}.
524        let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
525        for entry in self.rules.values() {
526            for premise in entry.rule.positive_body() {
527                if all_heads.contains(premise) {
528                    adj.entry(premise)
529                        .or_default()
530                        .push(entry.rule.head.as_str());
531                }
532            }
533        }
534
535        // Iterative DFS to find all simple cycles (report each once).
536        let mut found_cycles: Vec<Vec<String>> = Vec::new();
537        let mut globally_visited: HashSet<&str> = HashSet::new();
538
539        for start in all_heads.iter().copied() {
540            if globally_visited.contains(start) {
541                continue;
542            }
543
544            // DFS stack: (node, path_so_far, visited_in_this_path)
545            let mut stack: VecDeque<(&str, Vec<&str>, HashSet<&str>)> = VecDeque::new();
546            let mut path_visited: HashSet<&str> = HashSet::new();
547            path_visited.insert(start);
548            stack.push_back((start, vec![start], path_visited));
549
550            while let Some((node, path, mut visited)) = stack.pop_back() {
551                globally_visited.insert(node);
552
553                if let Some(neighbors) = adj.get(node) {
554                    for &neighbor in neighbors {
555                        if neighbor == start {
556                            // Back-edge → cycle found.
557                            let mut cycle: Vec<String> =
558                                path.iter().map(|s| s.to_string()).collect();
559                            cycle.push(start.to_string());
560                            found_cycles.push(cycle);
561                        } else if !visited.contains(neighbor) {
562                            let mut new_path = path.clone();
563                            new_path.push(neighbor);
564                            let mut new_visited = visited.clone();
565                            new_visited.insert(neighbor);
566                            stack.push_back((neighbor, new_path, new_visited));
567                        }
568                    }
569                }
570
571                // Mark all path nodes as globally visited to avoid re-traversal.
572                for n in &path {
573                    visited.insert(n);
574                }
575            }
576        }
577
578        found_cycles
579    }
580
581    // ── Resolution ────────────────────────────────────────────────────────────
582
583    /// Resolve a single conflict record and return the winning rule ID.
584    ///
585    /// The resolution strategy is taken from `self.config.default_strategy`.
586    ///
587    /// # Errors
588    ///
589    /// * [`ResolverError::RuleNotFound`] when a rule referenced in the conflict
590    ///   no longer exists.
591    /// * [`ResolverError::UnresolvableConflict`] when the strategy is
592    ///   [`ResolutionStrategy::AskOracle`].
593    /// * [`ResolverError::CyclicDependency`] for cyclic conflicts (cannot be
594    ///   resolved automatically).
595    pub fn resolve(&mut self, conflict: &ConflictRecord) -> Result<String, ResolverError> {
596        let strategy = self.config.default_strategy.clone();
597        let winner = self.apply_strategy(conflict, &strategy)?;
598        self.stats.conflicts_resolved += 1;
599        if self.stats.unresolved_conflicts > 0 {
600            self.stats.unresolved_conflicts -= 1;
601        }
602        Ok(winner)
603    }
604
605    /// Resolve all conflicts returned by `detect_conflicts` and return each
606    /// result paired with its conflict record.
607    pub fn resolve_all(&mut self) -> Vec<(ConflictRecord, Result<String, ResolverError>)> {
608        let conflicts = self.detect_conflicts();
609        let mut results = Vec::with_capacity(conflicts.len());
610        for conflict in conflicts {
611            let res = {
612                let strategy = self.config.default_strategy.clone();
613                self.apply_strategy(&conflict, &strategy)
614            };
615            if res.is_ok() {
616                self.stats.conflicts_resolved += 1;
617                if self.stats.unresolved_conflicts > 0 {
618                    self.stats.unresolved_conflicts -= 1;
619                }
620            }
621            results.push((conflict, res));
622        }
623        results
624    }
625
626    /// Internal: apply `strategy` to `conflict` and return the winning rule ID.
627    fn apply_strategy(
628        &self,
629        conflict: &ConflictRecord,
630        strategy: &ResolutionStrategy,
631    ) -> Result<String, ResolverError> {
632        match strategy {
633            ResolutionStrategy::AskOracle => {
634                let (a, b) = self.conflict_pair_ids(&conflict.conflict_type)?;
635                Err(ResolverError::UnresolvableConflict {
636                    rule_a: a,
637                    rule_b: b,
638                })
639            }
640
641            ResolutionStrategy::Merge(_op) => {
642                // Merge is a future extension; for now treat like AskOracle.
643                let (a, b) = self.conflict_pair_ids(&conflict.conflict_type)?;
644                Err(ResolverError::UnresolvableConflict {
645                    rule_a: a,
646                    rule_b: b,
647                })
648            }
649
650            ResolutionStrategy::PriorityOrder => {
651                let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
652                let entry_a = self
653                    .rules
654                    .get(&id_a)
655                    .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
656                let entry_b = self
657                    .rules
658                    .get(&id_b)
659                    .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
660                Ok(if entry_a.rule.priority >= entry_b.rule.priority {
661                    id_a
662                } else {
663                    id_b
664                })
665            }
666
667            ResolutionStrategy::Specificity => {
668                let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
669                let entry_a = self
670                    .rules
671                    .get(&id_a)
672                    .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
673                let entry_b = self
674                    .rules
675                    .get(&id_b)
676                    .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
677                Ok(if entry_a.rule.body.len() >= entry_b.rule.body.len() {
678                    id_a
679                } else {
680                    id_b
681                })
682            }
683
684            ResolutionStrategy::LastWriter => {
685                let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
686                let entry_a = self
687                    .rules
688                    .get(&id_a)
689                    .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
690                let entry_b = self
691                    .rules
692                    .get(&id_b)
693                    .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
694                Ok(if entry_a.insertion_order >= entry_b.insertion_order {
695                    id_a
696                } else {
697                    id_b
698                })
699            }
700
701            ResolutionStrategy::Inhibit => {
702                let (id_a, id_b) = self.conflict_pair_ids(&conflict.conflict_type)?;
703                let entry_a = self
704                    .rules
705                    .get(&id_a)
706                    .ok_or_else(|| ResolverError::RuleNotFound(id_a.clone()))?;
707                let entry_b = self
708                    .rules
709                    .get(&id_b)
710                    .ok_or_else(|| ResolverError::RuleNotFound(id_b.clone()))?;
711
712                let a_defeasible = entry_a.rule.is_defeasible;
713                let b_defeasible = entry_b.rule.is_defeasible;
714
715                let winner = match (a_defeasible, b_defeasible) {
716                    (true, false) => id_b.clone(),
717                    (false, true) => id_a.clone(),
718                    // Both or neither defeasible → fall back to priority.
719                    _ => {
720                        if entry_a.rule.priority >= entry_b.rule.priority {
721                            id_a.clone()
722                        } else {
723                            id_b.clone()
724                        }
725                    }
726                };
727                Ok(winner)
728            }
729        }
730    }
731
732    /// Extract the two participating rule IDs from a conflict type.
733    ///
734    /// For cyclic dependencies the "pair" is the first two elements of the cycle
735    /// (for error reporting purposes).
736    fn conflict_pair_ids(&self, ct: &ConflictType) -> Result<(String, String), ResolverError> {
737        match ct {
738            ConflictType::DirectContradiction { rule_a, rule_b } => {
739                Ok((rule_a.clone(), rule_b.clone()))
740            }
741            ConflictType::PriorityConflict { higher, lower } => Ok((higher.clone(), lower.clone())),
742            ConflictType::CyclicDependency { cycle } => {
743                if cycle.len() < 2 {
744                    return Err(ResolverError::CyclicDependency(cycle.clone()));
745                }
746                Err(ResolverError::CyclicDependency(cycle.clone()))
747            }
748            ConflictType::UndercutConflict {
749                undercutter,
750                undercut,
751            } => Ok((undercutter.clone(), undercut.clone())),
752            ConflictType::RebuttalConflict { rule_a, rule_b } => {
753                Ok((rule_a.clone(), rule_b.clone()))
754            }
755        }
756    }
757
758    // ── Query helpers ─────────────────────────────────────────────────────────
759
760    /// Return all rules whose positive body conditions are a subset of `facts`.
761    ///
762    /// Negated body conditions (prefixed with `"NOT:"`) are intentionally
763    /// ignored during this applicability check.
764    pub fn applicable_rules<'a>(&'a self, facts: &[String]) -> Vec<&'a LogicRule> {
765        let fact_set: HashSet<&str> = facts.iter().map(String::as_str).collect();
766        let mut result: Vec<&'a LogicRule> = self
767            .rules
768            .values()
769            .filter(|entry| entry.rule.positive_body().all(|p| fact_set.contains(p)))
770            .map(|entry| &entry.rule)
771            .collect();
772        // Stable order by insertion index.
773        result.sort_by_key(|r| self.rules.get(&r.id).map_or(0, |e| e.insertion_order));
774        result
775    }
776
777    /// Among the applicable rules for `head`, pick the winner according to the
778    /// configured default strategy.
779    ///
780    /// Returns `None` when no rule for that head is applicable.
781    pub fn winning_rule<'a>(&'a self, head: &str, facts: &[String]) -> Option<&'a LogicRule> {
782        let candidates: Vec<&'a LogicRule> = self
783            .applicable_rules(facts)
784            .into_iter()
785            .filter(|r| r.head == head)
786            .collect();
787
788        if candidates.is_empty() {
789            return None;
790        }
791
792        let winner = match &self.config.default_strategy {
793            ResolutionStrategy::PriorityOrder => candidates.into_iter().max_by_key(|r| r.priority),
794
795            ResolutionStrategy::Specificity => candidates.into_iter().max_by_key(|r| r.body.len()),
796
797            ResolutionStrategy::LastWriter => candidates
798                .into_iter()
799                .max_by_key(|r| self.rules.get(&r.id).map_or(0, |e| e.insertion_order)),
800
801            ResolutionStrategy::Inhibit => {
802                // Non-defeasible rules take precedence; among ties use priority.
803                let non_def: Vec<&'a LogicRule> = candidates
804                    .iter()
805                    .copied()
806                    .filter(|r| !r.is_defeasible)
807                    .collect();
808                if !non_def.is_empty() {
809                    non_def.into_iter().max_by_key(|r| r.priority)
810                } else {
811                    candidates.into_iter().max_by_key(|r| r.priority)
812                }
813            }
814
815            // For oracle / merge: fall back to priority.
816            ResolutionStrategy::AskOracle | ResolutionStrategy::Merge(_) => {
817                candidates.into_iter().max_by_key(|r| r.priority)
818            }
819        };
820
821        winner
822    }
823
824    // ── Statistics ────────────────────────────────────────────────────────────
825
826    /// Return a snapshot of current resolver statistics.
827    pub fn stats(&self) -> ResolverStats {
828        self.stats.clone()
829    }
830}
831
832// ─── Tests ────────────────────────────────────────────────────────────────────
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837
838    // ── Helpers ───────────────────────────────────────────────────────────────
839
840    fn make_rule(id: &str, head: &str, body: &[&str], priority: i32) -> LogicRule {
841        LogicRule {
842            id: id.to_string(),
843            head: head.to_string(),
844            body: body.iter().map(|s| s.to_string()).collect(),
845            priority,
846            source: "test".to_string(),
847            is_defeasible: false,
848        }
849    }
850
851    fn make_defeasible(id: &str, head: &str, body: &[&str], priority: i32) -> LogicRule {
852        LogicRule {
853            is_defeasible: true,
854            ..make_rule(id, head, body, priority)
855        }
856    }
857
858    fn default_resolver() -> RuleConflictResolver {
859        RuleConflictResolver::new(ResolverConfig::default())
860    }
861
862    // ── xorshift64 ────────────────────────────────────────────────────────────
863
864    #[test]
865    fn xorshift_produces_nonzero() {
866        let mut state = 12345u64;
867        let v = xorshift64(&mut state);
868        assert_ne!(v, 0);
869    }
870
871    #[test]
872    fn xorshift_sequence_differs() {
873        let mut state = 99u64;
874        let a = xorshift64(&mut state);
875        let b = xorshift64(&mut state);
876        assert_ne!(a, b);
877    }
878
879    // ── add_rule ──────────────────────────────────────────────────────────────
880
881    #[test]
882    fn add_rule_success() {
883        let mut r = default_resolver();
884        let rule = make_rule("r1", "bird", &["has_wings", "feathers"], 10);
885        assert!(r.add_rule(rule).is_ok());
886        assert_eq!(r.stats().rules_loaded, 1);
887    }
888
889    #[test]
890    fn add_multiple_rules() {
891        let mut r = default_resolver();
892        for i in 0..5u32 {
893            let rule = make_rule(&format!("r{i}"), "head", &[&format!("body{i}")], i as i32);
894            assert!(r.add_rule(rule).is_ok());
895        }
896        assert_eq!(r.stats().rules_loaded, 5);
897    }
898
899    #[test]
900    fn add_rule_max_exceeded() {
901        let cfg = ResolverConfig {
902            max_rules: 2,
903            ..Default::default()
904        };
905        let mut r = RuleConflictResolver::new(cfg);
906        r.add_rule(make_rule("r1", "a", &["x"], 1))
907            .expect("test setup: add_rule should not fail");
908        r.add_rule(make_rule("r2", "b", &["y"], 1))
909            .expect("test setup: add_rule should not fail");
910        let err = r
911            .add_rule(make_rule("r3", "c", &["z"], 1))
912            .expect_err("test setup: expected MaxRulesExceeded error");
913        assert_eq!(err, ResolverError::MaxRulesExceeded);
914    }
915
916    // ── remove_rule ───────────────────────────────────────────────────────────
917
918    #[test]
919    fn remove_existing_rule() {
920        let mut r = default_resolver();
921        r.add_rule(make_rule("r1", "bird", &["wings"], 10))
922            .expect("test setup: add_rule should not fail");
923        assert!(r.remove_rule("r1").is_ok());
924        assert_eq!(r.stats().rules_loaded, 0);
925    }
926
927    #[test]
928    fn remove_nonexistent_returns_error() {
929        let mut r = default_resolver();
930        let err = r
931            .remove_rule("ghost")
932            .expect_err("test setup: expected RuleNotFound error");
933        assert_eq!(err, ResolverError::RuleNotFound("ghost".to_string()));
934    }
935
936    #[test]
937    fn remove_reduces_count() {
938        let mut r = default_resolver();
939        r.add_rule(make_rule("r1", "a", &["x"], 1))
940            .expect("test setup: add_rule should not fail");
941        r.add_rule(make_rule("r2", "b", &["y"], 1))
942            .expect("test setup: add_rule should not fail");
943        r.remove_rule("r1")
944            .expect("test setup: remove_rule should not fail");
945        assert_eq!(r.stats().rules_loaded, 1);
946    }
947
948    // ── DirectContradiction ───────────────────────────────────────────────────
949
950    #[test]
951    fn detect_direct_contradiction() {
952        let mut r = default_resolver();
953        r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 10))
954            .expect("test setup: add_rule should not fail");
955        r.add_rule(make_rule("r2", "bird", &["lays_eggs"], 5))
956            .expect("test setup: add_rule should not fail");
957        let conflicts = r.detect_conflicts();
958        let has_contradiction = conflicts
959            .iter()
960            .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
961        assert!(has_contradiction);
962    }
963
964    #[test]
965    fn no_contradiction_when_bodies_overlap() {
966        let mut r = default_resolver();
967        // Both bodies share "vertebrate" → not disjoint.
968        r.add_rule(make_rule("r1", "bird", &["vertebrate", "wings"], 10))
969            .expect("test setup: add_rule should not fail");
970        r.add_rule(make_rule("r2", "bird", &["vertebrate", "feathers"], 5))
971            .expect("test setup: add_rule should not fail");
972        let conflicts = r.detect_conflicts();
973        let has_contradiction = conflicts
974            .iter()
975            .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
976        assert!(!has_contradiction);
977    }
978
979    #[test]
980    fn contradiction_ids_are_correct() {
981        let mut r = default_resolver();
982        r.add_rule(make_rule("alpha", "P", &["A"], 1))
983            .expect("test setup: add_rule should not fail");
984        r.add_rule(make_rule("beta", "P", &["B"], 1))
985            .expect("test setup: add_rule should not fail");
986        let conflicts = r.detect_conflicts();
987        let ct = conflicts
988            .iter()
989            .find_map(|c| {
990                if let ConflictType::DirectContradiction { rule_a, rule_b } = &c.conflict_type {
991                    Some((rule_a.clone(), rule_b.clone()))
992                } else {
993                    None
994                }
995            })
996            .expect("should have contradiction");
997        let ids: HashSet<String> = [ct.0, ct.1].into_iter().collect();
998        assert!(ids.contains("alpha"));
999        assert!(ids.contains("beta"));
1000    }
1001
1002    // ── PriorityConflict ──────────────────────────────────────────────────────
1003
1004    #[test]
1005    fn detect_priority_conflict() {
1006        let mut r = default_resolver();
1007        r.add_rule(make_rule("r1", "q", &["a"], 10))
1008            .expect("test setup: add_rule should not fail");
1009        r.add_rule(make_rule("r2", "q", &["b"], 5))
1010            .expect("test setup: add_rule should not fail");
1011        let conflicts = r.detect_conflicts();
1012        let has_prio = conflicts
1013            .iter()
1014            .any(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }));
1015        assert!(has_prio);
1016    }
1017
1018    #[test]
1019    fn no_priority_conflict_when_same_priority() {
1020        let mut r = default_resolver();
1021        // Both have priority 7 but disjoint bodies; only DirectContradiction.
1022        r.add_rule(make_rule("r1", "q", &["a"], 7))
1023            .expect("test setup: add_rule should not fail");
1024        r.add_rule(make_rule("r2", "q", &["b"], 7))
1025            .expect("test setup: add_rule should not fail");
1026        let conflicts = r.detect_conflicts();
1027        let has_prio = conflicts
1028            .iter()
1029            .any(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }));
1030        assert!(!has_prio);
1031    }
1032
1033    #[test]
1034    fn priority_conflict_higher_lower_correct() {
1035        let mut r = default_resolver();
1036        r.add_rule(make_rule("lo", "q", &["a"], 2))
1037            .expect("test setup: add_rule should not fail");
1038        r.add_rule(make_rule("hi", "q", &["b"], 9))
1039            .expect("test setup: add_rule should not fail");
1040        let conflicts = r.detect_conflicts();
1041        let prio = conflicts
1042            .iter()
1043            .find_map(|c| {
1044                if let ConflictType::PriorityConflict { higher, lower } = &c.conflict_type {
1045                    Some((higher.clone(), lower.clone()))
1046                } else {
1047                    None
1048                }
1049            })
1050            .expect("should have priority conflict");
1051        assert_eq!(prio.0, "hi");
1052        assert_eq!(prio.1, "lo");
1053    }
1054
1055    // ── CyclicDependency ──────────────────────────────────────────────────────
1056
1057    #[test]
1058    fn detect_simple_cycle() {
1059        let mut r = default_resolver();
1060        // A → B → A
1061        r.add_rule(make_rule("r_ab", "B", &["A"], 1))
1062            .expect("test setup: add_rule should not fail");
1063        r.add_rule(make_rule("r_ba", "A", &["B"], 1))
1064            .expect("test setup: add_rule should not fail");
1065        let conflicts = r.detect_conflicts();
1066        let has_cycle = conflicts
1067            .iter()
1068            .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1069        assert!(has_cycle);
1070    }
1071
1072    #[test]
1073    fn no_cycle_in_dag() {
1074        let mut r = default_resolver();
1075        // A → B → C (no cycle)
1076        r.add_rule(make_rule("r1", "B", &["A"], 1))
1077            .expect("test setup: add_rule should not fail");
1078        r.add_rule(make_rule("r2", "C", &["B"], 1))
1079            .expect("test setup: add_rule should not fail");
1080        let conflicts = r.detect_conflicts();
1081        let has_cycle = conflicts
1082            .iter()
1083            .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1084        assert!(!has_cycle);
1085    }
1086
1087    #[test]
1088    fn cycle_detection_disabled() {
1089        let cfg = ResolverConfig {
1090            enable_cycle_detection: false,
1091            ..Default::default()
1092        };
1093        let mut r = RuleConflictResolver::new(cfg);
1094        r.add_rule(make_rule("r_ab", "B", &["A"], 1))
1095            .expect("test setup: add_rule should not fail");
1096        r.add_rule(make_rule("r_ba", "A", &["B"], 1))
1097            .expect("test setup: add_rule should not fail");
1098        let conflicts = r.detect_conflicts();
1099        let has_cycle = conflicts
1100            .iter()
1101            .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1102        assert!(!has_cycle);
1103    }
1104
1105    #[test]
1106    fn three_node_cycle() {
1107        let mut r = default_resolver();
1108        r.add_rule(make_rule("ab", "B", &["A"], 1))
1109            .expect("test setup: add_rule should not fail");
1110        r.add_rule(make_rule("bc", "C", &["B"], 1))
1111            .expect("test setup: add_rule should not fail");
1112        r.add_rule(make_rule("ca", "A", &["C"], 1))
1113            .expect("test setup: add_rule should not fail");
1114        let conflicts = r.detect_conflicts();
1115        let has_cycle = conflicts
1116            .iter()
1117            .any(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }));
1118        assert!(has_cycle);
1119    }
1120
1121    // ── UndercutConflict ──────────────────────────────────────────────────────
1122
1123    #[test]
1124    fn detect_undercut_conflict() {
1125        let mut r = default_resolver();
1126        // r1 concludes "flies"; r2 has "NOT:flies" in its body.
1127        r.add_rule(make_rule("r1", "flies", &["has_wings"], 10))
1128            .expect("test setup: add_rule should not fail");
1129        r.add_rule(make_rule("r2", "penguin", &["NOT:flies", "lays_eggs"], 5))
1130            .expect("test setup: add_rule should not fail");
1131        let conflicts = r.detect_conflicts();
1132        let has_undercut = conflicts
1133            .iter()
1134            .any(|c| matches!(&c.conflict_type, ConflictType::UndercutConflict { .. }));
1135        assert!(has_undercut);
1136    }
1137
1138    #[test]
1139    fn undercut_ids_correct() {
1140        let mut r = default_resolver();
1141        r.add_rule(make_rule("cutter", "X", &["p"], 5))
1142            .expect("test setup: add_rule should not fail");
1143        r.add_rule(make_rule("victim", "Y", &["NOT:X", "q"], 5))
1144            .expect("test setup: add_rule should not fail");
1145        let conflicts = r.detect_conflicts();
1146        let uc = conflicts
1147            .iter()
1148            .find_map(|c| {
1149                if let ConflictType::UndercutConflict {
1150                    undercutter,
1151                    undercut,
1152                } = &c.conflict_type
1153                {
1154                    Some((undercutter.clone(), undercut.clone()))
1155                } else {
1156                    None
1157                }
1158            })
1159            .expect("should have undercut conflict");
1160        assert_eq!(uc.0, "cutter");
1161        assert_eq!(uc.1, "victim");
1162    }
1163
1164    #[test]
1165    fn no_undercut_without_not_prefix() {
1166        let mut r = default_resolver();
1167        r.add_rule(make_rule("r1", "flies", &["wings"], 1))
1168            .expect("test setup: add_rule should not fail");
1169        // body uses "flies" (positive), not "NOT:flies".
1170        r.add_rule(make_rule("r2", "bird", &["flies", "feathers"], 1))
1171            .expect("test setup: add_rule should not fail");
1172        let conflicts = r.detect_conflicts();
1173        let has_undercut = conflicts
1174            .iter()
1175            .any(|c| matches!(&c.conflict_type, ConflictType::UndercutConflict { .. }));
1176        assert!(!has_undercut);
1177    }
1178
1179    // ── RebuttalConflict ──────────────────────────────────────────────────────
1180
1181    #[test]
1182    fn detect_rebuttal_conflict() {
1183        let mut r = default_resolver();
1184        r.add_rule(make_rule("r1", "bird", &["wings"], 5))
1185            .expect("test setup: add_rule should not fail");
1186        r.add_rule(make_rule("r2", "NOT:bird", &["penguin"], 5))
1187            .expect("test setup: add_rule should not fail");
1188        let conflicts = r.detect_conflicts();
1189        let has_rebuttal = conflicts
1190            .iter()
1191            .any(|c| matches!(&c.conflict_type, ConflictType::RebuttalConflict { .. }));
1192        assert!(has_rebuttal);
1193    }
1194
1195    #[test]
1196    fn rebuttal_ids_correct() {
1197        let mut r = default_resolver();
1198        r.add_rule(make_rule("pos", "alive", &["moving"], 1))
1199            .expect("test setup: add_rule should not fail");
1200        r.add_rule(make_rule("neg", "NOT:alive", &["still"], 1))
1201            .expect("test setup: add_rule should not fail");
1202        let conflicts = r.detect_conflicts();
1203        let rb = conflicts
1204            .iter()
1205            .find_map(|c| {
1206                if let ConflictType::RebuttalConflict { rule_a, rule_b } = &c.conflict_type {
1207                    Some((rule_a.clone(), rule_b.clone()))
1208                } else {
1209                    None
1210                }
1211            })
1212            .expect("should have rebuttal conflict");
1213        let ids: HashSet<String> = [rb.0, rb.1].into_iter().collect();
1214        assert!(ids.contains("pos"));
1215        assert!(ids.contains("neg"));
1216    }
1217
1218    #[test]
1219    fn no_rebuttal_without_not_head() {
1220        let mut r = default_resolver();
1221        r.add_rule(make_rule("r1", "bird", &["wings"], 1))
1222            .expect("test setup: add_rule should not fail");
1223        r.add_rule(make_rule("r2", "reptile", &["scales"], 1))
1224            .expect("test setup: add_rule should not fail");
1225        let conflicts = r.detect_conflicts();
1226        let has_rebuttal = conflicts
1227            .iter()
1228            .any(|c| matches!(&c.conflict_type, ConflictType::RebuttalConflict { .. }));
1229        assert!(!has_rebuttal);
1230    }
1231
1232    // ── Resolution: PriorityOrder ─────────────────────────────────────────────
1233
1234    #[test]
1235    fn resolve_priority_order_higher_wins() {
1236        let cfg = ResolverConfig {
1237            default_strategy: ResolutionStrategy::PriorityOrder,
1238            ..Default::default()
1239        };
1240        let mut r = RuleConflictResolver::new(cfg);
1241        r.add_rule(make_rule("lo", "q", &["a"], 3))
1242            .expect("test setup: add_rule should not fail");
1243        r.add_rule(make_rule("hi", "q", &["b"], 9))
1244            .expect("test setup: add_rule should not fail");
1245        let mut conflicts = r.detect_conflicts();
1246        let prio_conflict = conflicts
1247            .iter_mut()
1248            .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1249            .cloned()
1250            .expect("must find priority conflict");
1251        let winner = r
1252            .resolve(&prio_conflict)
1253            .expect("test setup: resolve should succeed");
1254        assert_eq!(winner, "hi");
1255    }
1256
1257    #[test]
1258    fn resolve_priority_order_equal_returns_first_id() {
1259        let cfg = ResolverConfig {
1260            default_strategy: ResolutionStrategy::PriorityOrder,
1261            ..Default::default()
1262        };
1263        let mut r = RuleConflictResolver::new(cfg);
1264        r.add_rule(make_rule("r1", "q", &["a"], 5))
1265            .expect("test setup: add_rule should not fail");
1266        r.add_rule(make_rule("r2", "q", &["b"], 5))
1267            .expect("test setup: add_rule should not fail");
1268        let mut conflicts = r.detect_conflicts();
1269        // Only direct contradiction (same priority).
1270        let dc = conflicts
1271            .iter_mut()
1272            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1273            .cloned()
1274            .expect("must find contradiction");
1275        let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1276        // Either is fine; just ensure no error.
1277        assert!(winner == "r1" || winner == "r2");
1278    }
1279
1280    // ── Resolution: Specificity ───────────────────────────────────────────────
1281
1282    #[test]
1283    fn resolve_specificity_more_conditions_wins() {
1284        let cfg = ResolverConfig {
1285            default_strategy: ResolutionStrategy::Specificity,
1286            ..Default::default()
1287        };
1288        let mut r = RuleConflictResolver::new(cfg);
1289        // Disjoint bodies (no shared premises) → DirectContradiction.
1290        // "specific" has more conditions, so it should win under Specificity.
1291        r.add_rule(make_rule("general", "bird", &["flies"], 1))
1292            .expect("test setup: add_rule should not fail");
1293        r.add_rule(make_rule(
1294            "specific",
1295            "bird",
1296            &["lays_eggs", "warm_blooded", "beak"],
1297            1,
1298        ))
1299        .expect("test setup: add_rule should not fail");
1300        let mut conflicts = r.detect_conflicts();
1301        let dc = conflicts
1302            .iter_mut()
1303            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1304            .cloned()
1305            .expect("test setup: should find DirectContradiction conflict");
1306        let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1307        assert_eq!(winner, "specific");
1308    }
1309
1310    // ── Resolution: LastWriter ────────────────────────────────────────────────
1311
1312    #[test]
1313    fn resolve_last_writer_newer_wins() {
1314        let cfg = ResolverConfig {
1315            default_strategy: ResolutionStrategy::LastWriter,
1316            ..Default::default()
1317        };
1318        let mut r = RuleConflictResolver::new(cfg);
1319        r.add_rule(make_rule("old", "q", &["a"], 5))
1320            .expect("test setup: add_rule should not fail");
1321        r.add_rule(make_rule("new", "q", &["b"], 5))
1322            .expect("test setup: add_rule should not fail");
1323        let mut conflicts = r.detect_conflicts();
1324        let dc = conflicts
1325            .iter_mut()
1326            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1327            .cloned()
1328            .expect("test setup: should find DirectContradiction conflict");
1329        let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1330        assert_eq!(winner, "new");
1331    }
1332
1333    // ── Resolution: Inhibit ───────────────────────────────────────────────────
1334
1335    #[test]
1336    fn resolve_inhibit_non_defeasible_wins() {
1337        let cfg = ResolverConfig {
1338            default_strategy: ResolutionStrategy::Inhibit,
1339            ..Default::default()
1340        };
1341        let mut r = RuleConflictResolver::new(cfg);
1342        r.add_rule(make_defeasible("def", "q", &["a"], 10))
1343            .expect("test setup: add_rule should not fail");
1344        r.add_rule(make_rule("firm", "q", &["b"], 1))
1345            .expect("test setup: add_rule should not fail");
1346        let mut conflicts = r.detect_conflicts();
1347        let dc = conflicts
1348            .iter_mut()
1349            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1350            .cloned()
1351            .expect("test setup: should find DirectContradiction conflict");
1352        let winner = r.resolve(&dc).expect("test setup: resolve should succeed");
1353        assert_eq!(winner, "firm");
1354    }
1355
1356    #[test]
1357    fn resolve_inhibit_both_defeasible_uses_priority() {
1358        let cfg = ResolverConfig {
1359            default_strategy: ResolutionStrategy::Inhibit,
1360            ..Default::default()
1361        };
1362        let mut r = RuleConflictResolver::new(cfg);
1363        r.add_rule(make_defeasible("lo", "q", &["a"], 2))
1364            .expect("test setup: add_rule should not fail");
1365        r.add_rule(make_defeasible("hi", "q", &["b"], 9))
1366            .expect("test setup: add_rule should not fail");
1367        let mut conflicts = r.detect_conflicts();
1368        let prio = conflicts
1369            .iter_mut()
1370            .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1371            .cloned()
1372            .expect("test setup: should find PriorityConflict conflict");
1373        let winner = r
1374            .resolve(&prio)
1375            .expect("test setup: resolve should succeed");
1376        assert_eq!(winner, "hi");
1377    }
1378
1379    #[test]
1380    fn resolve_inhibit_neither_defeasible_uses_priority() {
1381        let cfg = ResolverConfig {
1382            default_strategy: ResolutionStrategy::Inhibit,
1383            ..Default::default()
1384        };
1385        let mut r = RuleConflictResolver::new(cfg);
1386        r.add_rule(make_rule("lo", "q", &["a"], 2))
1387            .expect("test setup: add_rule should not fail");
1388        r.add_rule(make_rule("hi", "q", &["b"], 9))
1389            .expect("test setup: add_rule should not fail");
1390        let mut conflicts = r.detect_conflicts();
1391        let prio = conflicts
1392            .iter_mut()
1393            .find(|c| matches!(&c.conflict_type, ConflictType::PriorityConflict { .. }))
1394            .cloned()
1395            .expect("test setup: should find PriorityConflict conflict");
1396        let winner = r
1397            .resolve(&prio)
1398            .expect("test setup: resolve should succeed");
1399        assert_eq!(winner, "hi");
1400    }
1401
1402    // ── Resolution: AskOracle ─────────────────────────────────────────────────
1403
1404    #[test]
1405    fn resolve_ask_oracle_returns_error() {
1406        let cfg = ResolverConfig {
1407            default_strategy: ResolutionStrategy::AskOracle,
1408            ..Default::default()
1409        };
1410        let mut r = RuleConflictResolver::new(cfg);
1411        r.add_rule(make_rule("r1", "q", &["a"], 1))
1412            .expect("test setup: add_rule should not fail");
1413        r.add_rule(make_rule("r2", "q", &["b"], 1))
1414            .expect("test setup: add_rule should not fail");
1415        let mut conflicts = r.detect_conflicts();
1416        let dc = conflicts
1417            .iter_mut()
1418            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1419            .cloned()
1420            .expect("test setup: should find DirectContradiction conflict");
1421        let err = r
1422            .resolve(&dc)
1423            .expect_err("test setup: expected UnresolvableConflict error");
1424        assert!(matches!(err, ResolverError::UnresolvableConflict { .. }));
1425    }
1426
1427    // ── Resolution: Merge ─────────────────────────────────────────────────────
1428
1429    #[test]
1430    fn resolve_merge_returns_error() {
1431        let cfg = ResolverConfig {
1432            default_strategy: ResolutionStrategy::Merge("union".to_string()),
1433            ..Default::default()
1434        };
1435        let mut r = RuleConflictResolver::new(cfg);
1436        r.add_rule(make_rule("r1", "q", &["a"], 1))
1437            .expect("test setup: add_rule should not fail");
1438        r.add_rule(make_rule("r2", "q", &["b"], 1))
1439            .expect("test setup: add_rule should not fail");
1440        let mut conflicts = r.detect_conflicts();
1441        let dc = conflicts
1442            .iter_mut()
1443            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1444            .cloned()
1445            .expect("test setup: should find DirectContradiction conflict");
1446        let err = r
1447            .resolve(&dc)
1448            .expect_err("test setup: expected UnresolvableConflict error");
1449        assert!(matches!(err, ResolverError::UnresolvableConflict { .. }));
1450    }
1451
1452    // ── resolve_all ───────────────────────────────────────────────────────────
1453
1454    #[test]
1455    fn resolve_all_returns_results_for_all_conflicts() {
1456        let mut r = default_resolver();
1457        r.add_rule(make_rule("r1", "q", &["a"], 10))
1458            .expect("test setup: add_rule should not fail");
1459        r.add_rule(make_rule("r2", "q", &["b"], 5))
1460            .expect("test setup: add_rule should not fail");
1461        let results = r.resolve_all();
1462        assert!(!results.is_empty());
1463    }
1464
1465    #[test]
1466    fn resolve_all_increments_resolved_stats() {
1467        let mut r = default_resolver();
1468        r.add_rule(make_rule("r1", "q", &["a"], 10))
1469            .expect("test setup: add_rule should not fail");
1470        r.add_rule(make_rule("r2", "q", &["b"], 5))
1471            .expect("test setup: add_rule should not fail");
1472        r.resolve_all();
1473        assert!(r.stats().conflicts_resolved > 0);
1474    }
1475
1476    // ── applicable_rules ──────────────────────────────────────────────────────
1477
1478    #[test]
1479    fn applicable_rules_all_facts_present() {
1480        let mut r = default_resolver();
1481        r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 1))
1482            .expect("test setup: add_rule should not fail");
1483        let facts: Vec<String> = vec!["wings".to_string(), "feathers".to_string()];
1484        let applicable = r.applicable_rules(&facts);
1485        assert_eq!(applicable.len(), 1);
1486        assert_eq!(applicable[0].id, "r1");
1487    }
1488
1489    #[test]
1490    fn applicable_rules_missing_fact() {
1491        let mut r = default_resolver();
1492        r.add_rule(make_rule("r1", "bird", &["wings", "feathers"], 1))
1493            .expect("test setup: add_rule should not fail");
1494        let facts: Vec<String> = vec!["wings".to_string()]; // missing "feathers"
1495        let applicable = r.applicable_rules(&facts);
1496        assert!(applicable.is_empty());
1497    }
1498
1499    #[test]
1500    fn applicable_rules_ignores_negated_conditions() {
1501        let mut r = default_resolver();
1502        // body has a NOT: condition; applicable_rules should ignore it.
1503        r.add_rule(make_rule(
1504            "r1",
1505            "mammal",
1506            &["warm_blooded", "NOT:has_gills"],
1507            1,
1508        ))
1509        .expect("test setup: add_rule should not fail");
1510        let facts: Vec<String> = vec!["warm_blooded".to_string()];
1511        // "NOT:has_gills" is ignored; rule should be applicable.
1512        let applicable = r.applicable_rules(&facts);
1513        assert_eq!(applicable.len(), 1);
1514    }
1515
1516    #[test]
1517    fn applicable_rules_empty_body_always_applicable() {
1518        let mut r = default_resolver();
1519        r.add_rule(make_rule("r_always", "axiom", &[], 1))
1520            .expect("test setup: add_rule should not fail");
1521        let facts: Vec<String> = vec![];
1522        let applicable = r.applicable_rules(&facts);
1523        assert_eq!(applicable.len(), 1);
1524    }
1525
1526    #[test]
1527    fn applicable_rules_multiple() {
1528        let mut r = default_resolver();
1529        r.add_rule(make_rule("r1", "a", &["x"], 1))
1530            .expect("test setup: add_rule should not fail");
1531        r.add_rule(make_rule("r2", "b", &["y"], 1))
1532            .expect("test setup: add_rule should not fail");
1533        r.add_rule(make_rule("r3", "c", &["x", "y"], 1))
1534            .expect("test setup: add_rule should not fail");
1535        let facts = vec!["x".to_string(), "y".to_string()];
1536        let applicable = r.applicable_rules(&facts);
1537        assert_eq!(applicable.len(), 3);
1538    }
1539
1540    // ── winning_rule ──────────────────────────────────────────────────────────
1541
1542    #[test]
1543    fn winning_rule_priority_strategy() {
1544        let cfg = ResolverConfig {
1545            default_strategy: ResolutionStrategy::PriorityOrder,
1546            ..Default::default()
1547        };
1548        let mut r = RuleConflictResolver::new(cfg);
1549        r.add_rule(make_rule("lo", "q", &["a"], 2))
1550            .expect("test setup: add_rule should not fail");
1551        r.add_rule(make_rule("hi", "q", &["a"], 9))
1552            .expect("test setup: add_rule should not fail");
1553        let facts = vec!["a".to_string()];
1554        let winner = r.winning_rule("q", &facts).expect("must have winner");
1555        assert_eq!(winner.id, "hi");
1556    }
1557
1558    #[test]
1559    fn winning_rule_specificity_strategy() {
1560        let cfg = ResolverConfig {
1561            default_strategy: ResolutionStrategy::Specificity,
1562            ..Default::default()
1563        };
1564        let mut r = RuleConflictResolver::new(cfg);
1565        r.add_rule(make_rule("gen", "q", &["a"], 1))
1566            .expect("test setup: add_rule should not fail");
1567        r.add_rule(make_rule("spec", "q", &["a", "b", "c"], 1))
1568            .expect("test setup: add_rule should not fail");
1569        let facts = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1570        let winner = r.winning_rule("q", &facts).expect("must have winner");
1571        assert_eq!(winner.id, "spec");
1572    }
1573
1574    #[test]
1575    fn winning_rule_last_writer_strategy() {
1576        let cfg = ResolverConfig {
1577            default_strategy: ResolutionStrategy::LastWriter,
1578            ..Default::default()
1579        };
1580        let mut r = RuleConflictResolver::new(cfg);
1581        r.add_rule(make_rule("old", "q", &["a"], 1))
1582            .expect("test setup: add_rule should not fail");
1583        r.add_rule(make_rule("new", "q", &["a"], 1))
1584            .expect("test setup: add_rule should not fail");
1585        let facts = vec!["a".to_string()];
1586        let winner = r.winning_rule("q", &facts).expect("must have winner");
1587        assert_eq!(winner.id, "new");
1588    }
1589
1590    #[test]
1591    fn winning_rule_none_when_no_applicable() {
1592        let mut r = default_resolver();
1593        r.add_rule(make_rule("r1", "q", &["missing_fact"], 1))
1594            .expect("test setup: add_rule should not fail");
1595        let facts: Vec<String> = vec![];
1596        assert!(r.winning_rule("q", &facts).is_none());
1597    }
1598
1599    #[test]
1600    fn winning_rule_none_when_wrong_head() {
1601        let mut r = default_resolver();
1602        r.add_rule(make_rule("r1", "q", &["a"], 1))
1603            .expect("test setup: add_rule should not fail");
1604        let facts = vec!["a".to_string()];
1605        assert!(r.winning_rule("unrelated_head", &facts).is_none());
1606    }
1607
1608    #[test]
1609    fn winning_rule_inhibit_non_defeasible_preferred() {
1610        let cfg = ResolverConfig {
1611            default_strategy: ResolutionStrategy::Inhibit,
1612            ..Default::default()
1613        };
1614        let mut r = RuleConflictResolver::new(cfg);
1615        r.add_rule(make_defeasible("soft", "q", &["a"], 99))
1616            .expect("test setup: add_rule should not fail");
1617        r.add_rule(make_rule("hard", "q", &["a"], 1))
1618            .expect("test setup: add_rule should not fail");
1619        let facts = vec!["a".to_string()];
1620        let winner = r.winning_rule("q", &facts).expect("must have winner");
1621        assert_eq!(winner.id, "hard");
1622    }
1623
1624    // ── stats ─────────────────────────────────────────────────────────────────
1625
1626    #[test]
1627    fn stats_rules_loaded_accurate() {
1628        let mut r = default_resolver();
1629        assert_eq!(r.stats().rules_loaded, 0);
1630        r.add_rule(make_rule("r1", "a", &["x"], 1))
1631            .expect("test setup: add_rule should not fail");
1632        assert_eq!(r.stats().rules_loaded, 1);
1633        r.remove_rule("r1")
1634            .expect("test setup: remove_rule should not fail");
1635        assert_eq!(r.stats().rules_loaded, 0);
1636    }
1637
1638    #[test]
1639    fn stats_conflicts_detected_increments() {
1640        let mut r = default_resolver();
1641        r.add_rule(make_rule("r1", "q", &["a"], 1))
1642            .expect("test setup: add_rule should not fail");
1643        r.add_rule(make_rule("r2", "q", &["b"], 1))
1644            .expect("test setup: add_rule should not fail");
1645        let before = r.stats().conflicts_detected;
1646        r.detect_conflicts();
1647        let after = r.stats().conflicts_detected;
1648        assert!(after > before);
1649    }
1650
1651    #[test]
1652    fn stats_cycles_found() {
1653        let mut r = default_resolver();
1654        r.add_rule(make_rule("ab", "B", &["A"], 1))
1655            .expect("test setup: add_rule should not fail");
1656        r.add_rule(make_rule("ba", "A", &["B"], 1))
1657            .expect("test setup: add_rule should not fail");
1658        r.detect_conflicts();
1659        assert!(r.stats().cycles_found > 0);
1660    }
1661
1662    #[test]
1663    fn stats_conflicts_resolved() {
1664        let mut r = default_resolver();
1665        r.add_rule(make_rule("r1", "q", &["a"], 5))
1666            .expect("test setup: add_rule should not fail");
1667        r.add_rule(make_rule("r2", "q", &["b"], 10))
1668            .expect("test setup: add_rule should not fail");
1669        r.resolve_all();
1670        assert!(r.stats().conflicts_resolved > 0);
1671    }
1672
1673    // ── Error cases ───────────────────────────────────────────────────────────
1674
1675    #[test]
1676    fn resolve_rule_not_found_after_removal() {
1677        let mut r = default_resolver();
1678        r.add_rule(make_rule("r1", "q", &["a"], 5))
1679            .expect("test setup: add_rule should not fail");
1680        r.add_rule(make_rule("r2", "q", &["b"], 10))
1681            .expect("test setup: add_rule should not fail");
1682        let mut conflicts = r.detect_conflicts();
1683        let dc = conflicts
1684            .iter_mut()
1685            .find(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }))
1686            .cloned()
1687            .expect("test setup: should find DirectContradiction conflict");
1688        // Remove one rule before resolving.
1689        r.remove_rule("r1")
1690            .expect("test setup: remove_rule should not fail");
1691        let err = r
1692            .resolve(&dc)
1693            .expect_err("test setup: expected RuleNotFound error after removal");
1694        assert!(matches!(err, ResolverError::RuleNotFound(_)));
1695    }
1696
1697    #[test]
1698    fn cyclic_conflict_pair_error() {
1699        let mut r = default_resolver();
1700        r.add_rule(make_rule("ab", "B", &["A"], 1))
1701            .expect("test setup: add_rule should not fail");
1702        r.add_rule(make_rule("ba", "A", &["B"], 1))
1703            .expect("test setup: add_rule should not fail");
1704        let mut conflicts = r.detect_conflicts();
1705        let cycle_conflict = conflicts
1706            .iter_mut()
1707            .find(|c| matches!(&c.conflict_type, ConflictType::CyclicDependency { .. }))
1708            .cloned()
1709            .expect("test setup: should find CyclicDependency conflict");
1710        let err = r
1711            .resolve(&cycle_conflict)
1712            .expect_err("test setup: expected CyclicDependency error");
1713        assert!(matches!(err, ResolverError::CyclicDependency(_)));
1714    }
1715
1716    #[test]
1717    fn config_error_variant_display() {
1718        let err = ResolverError::ConfigurationError("bad value".to_string());
1719        assert!(err.to_string().contains("bad value"));
1720    }
1721
1722    #[test]
1723    fn max_rules_exceeded_display() {
1724        let err = ResolverError::MaxRulesExceeded;
1725        assert!(!err.to_string().is_empty());
1726    }
1727
1728    // ── LogicRule helpers ─────────────────────────────────────────────────────
1729
1730    #[test]
1731    fn positive_body_excludes_negations() {
1732        let rule = make_rule("r", "head", &["a", "NOT:b", "c"], 1);
1733        let pos: Vec<&str> = rule.positive_body().collect();
1734        assert_eq!(pos, vec!["a", "c"]);
1735    }
1736
1737    #[test]
1738    fn negated_body_strips_prefix() {
1739        let rule = make_rule("r", "head", &["a", "NOT:b", "NOT:c"], 1);
1740        let neg: Vec<&str> = rule.negated_body().collect();
1741        assert_eq!(neg, vec!["b", "c"]);
1742    }
1743
1744    // ── Edge cases ────────────────────────────────────────────────────────────
1745
1746    #[test]
1747    fn empty_resolver_no_conflicts() {
1748        let mut r = default_resolver();
1749        assert!(r.detect_conflicts().is_empty());
1750    }
1751
1752    #[test]
1753    fn single_rule_no_conflicts() {
1754        let mut r = default_resolver();
1755        r.add_rule(make_rule("only", "q", &["a"], 1))
1756            .expect("test setup: add_rule should not fail");
1757        assert!(r.detect_conflicts().is_empty());
1758    }
1759
1760    #[test]
1761    fn different_heads_no_direct_contradiction() {
1762        let mut r = default_resolver();
1763        r.add_rule(make_rule("r1", "bird", &["wings"], 1))
1764            .expect("test setup: add_rule should not fail");
1765        r.add_rule(make_rule("r2", "fish", &["fins"], 1))
1766            .expect("test setup: add_rule should not fail");
1767        let conflicts = r.detect_conflicts();
1768        let has_dc = conflicts
1769            .iter()
1770            .any(|c| matches!(&c.conflict_type, ConflictType::DirectContradiction { .. }));
1771        assert!(!has_dc);
1772    }
1773
1774    #[test]
1775    fn resolve_all_empty_resolver() {
1776        let mut r = default_resolver();
1777        let results = r.resolve_all();
1778        assert!(results.is_empty());
1779    }
1780
1781    #[test]
1782    fn applicable_rules_superset_of_body() {
1783        let mut r = default_resolver();
1784        r.add_rule(make_rule("r1", "q", &["a"], 1))
1785            .expect("test setup: add_rule should not fail");
1786        // Provide more facts than needed.
1787        let facts = vec!["a".to_string(), "b".to_string(), "c".to_string()];
1788        let applicable = r.applicable_rules(&facts);
1789        assert_eq!(applicable.len(), 1);
1790    }
1791}