1use std::collections::HashMap;
59
60#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ConflictType {
65 HeadOverlap,
67 CycleDetected,
69 PriorityTie,
72 ContradictoryConstraints,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct RuleV2 {
81 pub rule_id: String,
83 pub head: String,
85 pub body: Vec<String>,
87 pub priority: i32,
89 pub author: String,
91 pub created_at_secs: u64,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct ConflictReport {
100 pub conflict_type: ConflictType,
102 pub rule_ids: Vec<String>,
104 pub winner: Option<String>,
106 pub resolution: String,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum ResolutionStrategy {
115 HigherPriority,
117 LaterTimestamp,
119 Alphabetical,
121 FirstRegistered,
123}
124
125#[derive(Debug, Clone)]
132pub struct RuleConflictResolverV2 {
133 pub rules: Vec<RuleV2>,
135 pub strategy: ResolutionStrategy,
137}
138
139impl RuleConflictResolverV2 {
140 pub fn new(strategy: ResolutionStrategy) -> Self {
142 Self {
143 rules: Vec::new(),
144 strategy,
145 }
146 }
147
148 pub fn register(&mut self, rule: RuleV2) {
153 self.rules.push(rule);
154 }
155
156 fn pick_winner_indices(&self, idx_a: usize, idx_b: usize) -> Option<usize> {
162 let a = &self.rules[idx_a];
163 let b = &self.rules[idx_b];
164 match self.strategy {
165 ResolutionStrategy::HigherPriority => {
166 if a.priority > b.priority {
167 Some(idx_a)
168 } else if b.priority > a.priority {
169 Some(idx_b)
170 } else {
171 None }
173 }
174 ResolutionStrategy::LaterTimestamp => {
175 if a.created_at_secs > b.created_at_secs {
176 Some(idx_a)
177 } else if b.created_at_secs > a.created_at_secs {
178 Some(idx_b)
179 } else {
180 None
181 }
182 }
183 ResolutionStrategy::Alphabetical => {
184 if a.rule_id <= b.rule_id {
185 Some(idx_a)
186 } else {
187 Some(idx_b)
188 }
189 }
190 ResolutionStrategy::FirstRegistered => {
191 if idx_a <= idx_b {
194 Some(idx_a)
195 } else {
196 Some(idx_b)
197 }
198 }
199 }
200 }
201
202 pub fn detect_head_overlaps(&self) -> Vec<ConflictReport> {
210 let mut reports = Vec::new();
211
212 for i in 0..self.rules.len() {
213 for j in (i + 1)..self.rules.len() {
214 let head_a = &self.rules[i].head;
215 let head_b = &self.rules[j].head;
216
217 let overlaps =
218 head_a.starts_with(head_b.as_str()) || head_b.starts_with(head_a.as_str());
219 if !overlaps {
220 continue;
221 }
222
223 let (conflict_type, winner_id, resolution) =
225 if matches!(self.strategy, ResolutionStrategy::HigherPriority)
226 && self.rules[i].priority == self.rules[j].priority
227 {
228 let res = format!(
229 "Rules '{}' and '{}' have overlapping heads ('{}' / '{}') \
230 and equal priority {}; cannot determine winner.",
231 self.rules[i].rule_id,
232 self.rules[j].rule_id,
233 head_a,
234 head_b,
235 self.rules[i].priority
236 );
237 (ConflictType::PriorityTie, None, res)
238 } else {
239 let winner_idx = self.pick_winner_indices(i, j);
240 let winner_id = winner_idx.map(|idx| self.rules[idx].rule_id.clone());
241 let res = match &winner_id {
242 Some(w) => format!(
243 "Rules '{}' and '{}' have overlapping heads ('{}' / '{}'). \
244 Winner: '{}' selected by {:?} strategy.",
245 self.rules[i].rule_id,
246 self.rules[j].rule_id,
247 head_a,
248 head_b,
249 w,
250 self.strategy
251 ),
252 None => format!(
253 "Rules '{}' and '{}' have overlapping heads ('{}' / '{}'). \
254 Strategy {:?} could not break the tie.",
255 self.rules[i].rule_id,
256 self.rules[j].rule_id,
257 head_a,
258 head_b,
259 self.strategy
260 ),
261 };
262 (ConflictType::HeadOverlap, winner_id, res)
263 };
264
265 reports.push(ConflictReport {
266 conflict_type,
267 rule_ids: vec![self.rules[i].rule_id.clone(), self.rules[j].rule_id.clone()],
268 winner: winner_id,
269 resolution,
270 });
271 }
272 }
273
274 reports
275 }
276
277 pub fn detect_cycles(&self) -> Vec<ConflictReport> {
289 let mut head_to_indices: HashMap<&str, Vec<usize>> = HashMap::new();
291 for (idx, rule) in self.rules.iter().enumerate() {
292 head_to_indices
293 .entry(rule.head.as_str())
294 .or_default()
295 .push(idx);
296 }
297
298 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); self.rules.len()];
300 for (src_idx, rule) in self.rules.iter().enumerate() {
301 for body_pred in &rule.body {
302 if let Some(targets) = head_to_indices.get(body_pred.as_str()) {
303 for &tgt_idx in targets {
304 if tgt_idx != src_idx {
305 adj[src_idx].push(tgt_idx);
306 }
307 }
308 }
309 }
310 }
311
312 let n = self.rules.len();
313 let mut color: Vec<u8> = vec![0u8; n];
315 let mut parent: Vec<Option<usize>> = vec![None; n];
317 let mut reports: Vec<ConflictReport> = Vec::new();
318 let mut reported_cycles: Vec<Vec<usize>> = Vec::new();
320
321 for start in 0..n {
322 if color[start] != 0 {
323 continue;
324 }
325 let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
328 color[start] = 1;
329
330 'dfs: while let Some((node, child_pos)) = stack.last_mut() {
331 let node = *node;
332 let adj_list = &adj[node];
333
334 if *child_pos < adj_list.len() {
335 let child = adj_list[*child_pos];
336 *child_pos += 1;
337
338 if color[child] == 1 {
339 let mut cycle_indices: Vec<usize> = Vec::new();
341 cycle_indices.push(child);
342 let mut found = false;
344 for &(frame_node, _) in stack.iter().rev() {
345 if found {
346 break;
347 }
348 cycle_indices.push(frame_node);
349 if frame_node == child {
350 found = true;
351 }
352 }
353 cycle_indices.dedup();
354
355 let mut cycle_sorted = cycle_indices.clone();
358 cycle_sorted.sort_unstable();
359 cycle_sorted.dedup();
360
361 let already = reported_cycles.contains(&cycle_sorted);
362
363 if !already {
364 reported_cycles.push(cycle_sorted);
365 let rule_ids: Vec<String> = cycle_indices
366 .iter()
367 .map(|&idx| self.rules[idx].rule_id.clone())
368 .collect();
369 let resolution = format!(
370 "Dependency cycle detected among rules: {}.",
371 rule_ids.join(" → ")
372 );
373 reports.push(ConflictReport {
374 conflict_type: ConflictType::CycleDetected,
375 rule_ids,
376 winner: None,
377 resolution,
378 });
379 }
380 } else if color[child] == 0 {
381 color[child] = 1;
382 parent[child] = Some(node);
383 stack.push((child, 0));
384 }
385 } else {
387 color[node] = 2;
389 stack.pop();
390 let _ = &adj; continue 'dfs;
392 }
393 }
394 }
395
396 let _ = parent;
399
400 reports
401 }
402
403 pub fn resolve_all(&self) -> Vec<ConflictReport> {
408 let mut reports = self.detect_head_overlaps();
409 let cycles = self.detect_cycles();
410 for cycle in cycles {
411 let already = reports
413 .iter()
414 .any(|r| r.conflict_type == cycle.conflict_type && r.rule_ids == cycle.rule_ids);
415 if !already {
416 reports.push(cycle);
417 }
418 }
419 reports
420 }
421
422 pub fn winner_for_goal(&self, goal: &str) -> Option<&RuleV2> {
431 let candidates: Vec<usize> = self
433 .rules
434 .iter()
435 .enumerate()
436 .filter(|(_, r)| goal.starts_with(r.head.as_str()))
437 .map(|(idx, _)| idx)
438 .collect();
439
440 if candidates.is_empty() {
441 return None;
442 }
443
444 let best_idx = candidates.iter().copied().reduce(|acc, idx| {
446 match self.pick_winner_indices(acc, idx) {
447 Some(winner) => winner,
448 None => acc,
450 }
451 })?;
452
453 self.rules.get(best_idx)
454 }
455
456 pub fn rules_for_author(&self, author: &str) -> Vec<&RuleV2> {
458 self.rules.iter().filter(|r| r.author == author).collect()
459 }
460
461 pub fn sorted_by_priority(&self) -> Vec<&RuleV2> {
466 let mut sorted: Vec<&RuleV2> = self.rules.iter().collect();
467 sorted.sort_by_key(|r| std::cmp::Reverse(r.priority)); sorted
469 }
470}
471
472#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn make_rule(
481 id: &str,
482 head: &str,
483 body: Vec<&str>,
484 priority: i32,
485 author: &str,
486 ts: u64,
487 ) -> RuleV2 {
488 RuleV2 {
489 rule_id: id.to_string(),
490 head: head.to_string(),
491 body: body.iter().map(|s| s.to_string()).collect(),
492 priority,
493 author: author.to_string(),
494 created_at_secs: ts,
495 }
496 }
497
498 #[test]
501 fn test_register_adds_rule() {
502 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
503 assert_eq!(resolver.rules.len(), 0);
504 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
505 assert_eq!(resolver.rules.len(), 1);
506 resolver.register(make_rule("r2", "child", vec![], 3, "bob", 2000));
507 assert_eq!(resolver.rules.len(), 2);
508 }
509
510 #[test]
513 fn test_no_conflicts_non_overlapping() {
514 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
515 resolver.register(make_rule("r1", "ancestor", vec![], 5, "alice", 1000));
516 resolver.register(make_rule("r2", "sibling", vec![], 3, "bob", 2000));
517 let reports = resolver.detect_head_overlaps();
518 assert!(reports.is_empty());
519 }
520
521 #[test]
524 fn test_head_overlap_prefix_match() {
525 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
526 resolver.register(make_rule("r1", "parent", vec![], 10, "alice", 1000));
527 resolver.register(make_rule("r2", "parent_of", vec![], 5, "bob", 2000));
528 let reports = resolver.detect_head_overlaps();
529 assert_eq!(reports.len(), 1);
530 assert!(matches!(
531 reports[0].conflict_type,
532 ConflictType::HeadOverlap
533 ));
534 assert!(reports[0].rule_ids.contains(&"r1".to_string()));
535 assert!(reports[0].rule_ids.contains(&"r2".to_string()));
536 }
537
538 #[test]
541 fn test_higher_priority_wins() {
542 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
543 resolver.register(make_rule("r1", "parent", vec![], 10, "alice", 1000));
544 resolver.register(make_rule("r2", "parent_of", vec![], 5, "bob", 2000));
545 let reports = resolver.detect_head_overlaps();
546 assert_eq!(reports.len(), 1);
547 assert_eq!(reports[0].winner, Some("r1".to_string()));
548 }
549
550 #[test]
553 fn test_priority_tie_generates_report() {
554 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
555 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
556 resolver.register(make_rule("r2", "parent_of", vec![], 5, "bob", 2000));
557 let reports = resolver.detect_head_overlaps();
558 assert_eq!(reports.len(), 1);
559 assert!(matches!(
560 reports[0].conflict_type,
561 ConflictType::PriorityTie
562 ));
563 assert_eq!(reports[0].winner, None);
564 }
565
566 #[test]
569 fn test_later_timestamp_strategy() {
570 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::LaterTimestamp);
571 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
572 resolver.register(make_rule("r2", "parent_of", vec![], 5, "bob", 9999));
573 let reports = resolver.detect_head_overlaps();
574 assert_eq!(reports.len(), 1);
575 assert_eq!(reports[0].winner, Some("r2".to_string()));
576 }
577
578 #[test]
581 fn test_alphabetical_strategy() {
582 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::Alphabetical);
583 resolver.register(make_rule("zebra_rule", "parent", vec![], 5, "alice", 1000));
584 resolver.register(make_rule("alpha_rule", "parent_of", vec![], 5, "bob", 2000));
585 let reports = resolver.detect_head_overlaps();
586 assert_eq!(reports.len(), 1);
587 assert_eq!(reports[0].winner, Some("alpha_rule".to_string()));
588 }
589
590 #[test]
593 fn test_first_registered_strategy() {
594 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::FirstRegistered);
595 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
596 resolver.register(make_rule("r2", "parent_of", vec![], 5, "bob", 2000));
597 let reports = resolver.detect_head_overlaps();
598 assert_eq!(reports.len(), 1);
599 assert_eq!(reports[0].winner, Some("r1".to_string()));
600 }
601
602 #[test]
605 fn test_cycle_ab_ba() {
606 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
607 resolver.register(make_rule("A", "head_a", vec!["head_b"], 5, "alice", 1000));
609 resolver.register(make_rule("B", "head_b", vec!["head_a"], 5, "alice", 2000));
610 let reports = resolver.detect_cycles();
611 assert!(!reports.is_empty());
612 assert!(matches!(
613 reports[0].conflict_type,
614 ConflictType::CycleDetected
615 ));
616 let ids: Vec<&str> = reports[0].rule_ids.iter().map(|s| s.as_str()).collect();
618 assert!(ids.contains(&"A"));
619 assert!(ids.contains(&"B"));
620 }
621
622 #[test]
625 fn test_cycle_abc() {
626 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
627 resolver.register(make_rule("A", "head_a", vec!["head_b"], 5, "alice", 1000));
628 resolver.register(make_rule("B", "head_b", vec!["head_c"], 5, "alice", 2000));
629 resolver.register(make_rule("C", "head_c", vec!["head_a"], 5, "alice", 3000));
630 let reports = resolver.detect_cycles();
631 assert!(!reports.is_empty());
632 let ids: Vec<&str> = reports[0].rule_ids.iter().map(|s| s.as_str()).collect();
633 assert!(ids.contains(&"A"));
634 assert!(ids.contains(&"B"));
635 assert!(ids.contains(&"C"));
636 }
637
638 #[test]
641 fn test_no_cycle_chain_abc() {
642 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
643 resolver.register(make_rule("A", "head_a", vec!["head_b"], 5, "alice", 1000));
644 resolver.register(make_rule("B", "head_b", vec!["head_c"], 5, "alice", 2000));
645 resolver.register(make_rule("C", "head_c", vec![], 5, "alice", 3000));
646 let reports = resolver.detect_cycles();
647 assert!(reports.is_empty());
648 }
649
650 #[test]
653 fn test_resolve_all_combines_reports() {
654 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
655 resolver.register(make_rule("r1", "parent", vec!["head_b"], 10, "alice", 1000));
657 resolver.register(make_rule("r2", "parent_of", vec!["head_a"], 5, "bob", 2000));
658 resolver.register(make_rule(
661 "cA",
662 "cycle_a",
663 vec!["cycle_b"],
664 1,
665 "carol",
666 3000,
667 ));
668 resolver.register(make_rule(
669 "cB",
670 "cycle_b",
671 vec!["cycle_a"],
672 1,
673 "carol",
674 4000,
675 ));
676 let reports = resolver.resolve_all();
677 let has_overlap = reports.iter().any(|r| {
678 r.conflict_type == ConflictType::HeadOverlap
679 || r.conflict_type == ConflictType::PriorityTie
680 });
681 let has_cycle = reports
682 .iter()
683 .any(|r| r.conflict_type == ConflictType::CycleDetected);
684 assert!(
685 has_overlap,
686 "Expected at least one HeadOverlap or PriorityTie report"
687 );
688 assert!(has_cycle, "Expected at least one CycleDetected report");
689 }
690
691 #[test]
694 fn test_winner_for_goal_highest_priority() {
695 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
696 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
697 resolver.register(make_rule("r2", "parent", vec![], 10, "bob", 2000));
698 resolver.register(make_rule("r3", "parent_extra", vec![], 3, "carol", 3000));
699 let winner = resolver.winner_for_goal("parent(X,Y)");
700 assert!(winner.is_some());
701 assert_eq!(winner.map(|r| r.rule_id.as_str()), Some("r2"));
702 }
703
704 #[test]
707 fn test_winner_for_goal_unknown() {
708 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
709 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
710 let winner = resolver.winner_for_goal("ancestor(X,Y)");
711 assert!(winner.is_none());
712 }
713
714 #[test]
717 fn test_rules_for_author() {
718 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
719 resolver.register(make_rule("r1", "parent", vec![], 5, "alice", 1000));
720 resolver.register(make_rule("r2", "child", vec![], 3, "bob", 2000));
721 resolver.register(make_rule("r3", "sibling", vec![], 4, "alice", 3000));
722 let alice_rules = resolver.rules_for_author("alice");
723 assert_eq!(alice_rules.len(), 2);
724 let ids: Vec<&str> = alice_rules.iter().map(|r| r.rule_id.as_str()).collect();
725 assert!(ids.contains(&"r1"));
726 assert!(ids.contains(&"r3"));
727 let bob_rules = resolver.rules_for_author("bob");
728 assert_eq!(bob_rules.len(), 1);
729 }
730
731 #[test]
734 fn test_sorted_by_priority_descending() {
735 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
736 resolver.register(make_rule("r1", "a", vec![], 3, "alice", 1000));
737 resolver.register(make_rule("r2", "b", vec![], 10, "bob", 2000));
738 resolver.register(make_rule("r3", "c", vec![], 7, "carol", 3000));
739 let sorted = resolver.sorted_by_priority();
740 assert_eq!(sorted[0].priority, 10);
741 assert_eq!(sorted[1].priority, 7);
742 assert_eq!(sorted[2].priority, 3);
743 }
744
745 #[test]
748 fn test_conflict_report_winner_field() {
749 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::LaterTimestamp);
750 resolver.register(make_rule("old_rule", "fact", vec![], 5, "alice", 100));
751 resolver.register(make_rule(
752 "new_rule",
753 "fact_detail",
754 vec![],
755 5,
756 "alice",
757 9999,
758 ));
759 let reports = resolver.detect_head_overlaps();
760 assert_eq!(reports.len(), 1);
761 assert_eq!(reports[0].winner, Some("new_rule".to_string()));
762 assert!(!reports[0].resolution.is_empty());
763 }
764
765 #[test]
768 fn test_head_overlap_exact_match() {
769 let mut resolver = RuleConflictResolverV2::new(ResolutionStrategy::HigherPriority);
770 resolver.register(make_rule("r1", "same_head", vec![], 5, "alice", 1000));
771 resolver.register(make_rule("r2", "same_head", vec![], 8, "bob", 2000));
772 let reports = resolver.detect_head_overlaps();
773 assert_eq!(reports.len(), 1);
774 assert!(matches!(
776 reports[0].conflict_type,
777 ConflictType::HeadOverlap | ConflictType::PriorityTie
778 ));
779 assert_eq!(reports[0].winner, Some("r2".to_string()));
781 }
782}