link_cli/changes_simplifier.rs
1//! ChangesSimplifier - Simplifies a list of changes
2//!
3//! This module provides functionality to simplify a list of changes by
4//! identifying chains of transformations.
5//! Corresponds to ChangesSimplifier.cs in C#
6
7use crate::link::Link;
8use std::collections::{HashMap, HashSet};
9
10/// Simplifies a list of changes by identifying chains of transformations.
11///
12/// If multiple final states are reachable from the same initial state, returns multiple simplified changes.
13/// If a scenario arises where no initial or final states can be identified (no-ops), returns the original transitions as-is.
14pub fn simplify_changes(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
15 if changes.is_empty() {
16 return vec![];
17 }
18
19 // **FIX for Issue #26**: Remove duplicate before states by keeping the non-null transitions
20 // This handles cases where the same link is reported with multiple different transformations
21 let changes = remove_duplicate_before_states(changes);
22
23 // First, handle unchanged states directly
24 let mut unchanged_states = Vec::new();
25 let mut changed_states = Vec::new();
26
27 for (before, after) in changes.iter() {
28 if before == after {
29 unchanged_states.push((*before, *after));
30 } else {
31 changed_states.push((*before, *after));
32 }
33 }
34
35 // Gather all 'Before' links and all 'After' links from changed states
36 let before_links: HashSet<Link> = changed_states.iter().map(|(b, _)| *b).collect();
37 let after_links: HashSet<Link> = changed_states.iter().map(|(_, a)| *a).collect();
38
39 // Identify initial states: appear as Before but never as After
40 let initial_states: Vec<Link> = before_links
41 .iter()
42 .filter(|b| !after_links.contains(b))
43 .copied()
44 .collect();
45
46 // Identify final states: appear as After but never as Before
47 let final_states: HashSet<Link> = after_links
48 .iter()
49 .filter(|a| !before_links.contains(a))
50 .copied()
51 .collect();
52
53 // Build adjacency (Before -> possible list of After links)
54 let mut adjacency: HashMap<Link, Vec<Link>> = HashMap::new();
55 for (before, after) in changed_states.iter() {
56 adjacency.entry(*before).or_default().push(*after);
57 }
58
59 // If we have no identified initial states, treat it as a no-op scenario:
60 // just return original transitions.
61 if initial_states.is_empty() {
62 return changes;
63 }
64
65 let mut results = Vec::new();
66
67 // Add unchanged states first
68 results.extend(unchanged_states);
69
70 // Traverse each initial state with DFS
71 for initial in initial_states.iter() {
72 let mut stack = vec![*initial];
73 let mut visited: HashSet<Link> = HashSet::new();
74
75 while let Some(current) = stack.pop() {
76 // Skip if already visited
77 if !visited.insert(current) {
78 continue;
79 }
80
81 let has_next = adjacency.contains_key(¤t);
82 let next_links = adjacency.get(¤t);
83 let is_final_or_dead_end = final_states.contains(¤t)
84 || !has_next
85 || next_links.is_none_or(|v| v.is_empty());
86
87 // If final or no further transitions, record (initial -> current)
88 if is_final_or_dead_end {
89 results.push((*initial, current));
90 }
91
92 // Otherwise push neighbors
93 if let Some(next_links) = next_links {
94 for next in next_links {
95 stack.push(*next);
96 }
97 }
98 }
99 }
100
101 // Sort the final results so that items appear in ascending order by their After link.
102 // This ensures tests that expect a specific order pass reliably.
103 results.sort_by(|a, b| {
104 a.1.index
105 .cmp(&b.1.index)
106 .then_with(|| a.1.source.cmp(&b.1.source))
107 .then_with(|| a.1.target.cmp(&b.1.target))
108 });
109
110 results
111}
112
113/// Removes problematic duplicate before states that lead to simplification issues.
114/// This fixes Issue #26 where multiple transformations from the same before state
115/// to conflicting after states (including null states) would cause the simplifier to fail.
116///
117/// The key insight: If we have multiple transitions from the same before state,
118/// and one of them is to a "null" state (0: 0 0), we should prefer the non-null transition
119/// as it represents the actual final transformation.
120fn remove_duplicate_before_states(changes: Vec<(Link, Link)>) -> Vec<(Link, Link)> {
121 // Group changes by their before state
122 let mut grouped: HashMap<Link, Vec<(Link, Link)>> = HashMap::new();
123 for change in changes {
124 grouped.entry(change.0).or_default().push(change);
125 }
126
127 let mut result = Vec::new();
128
129 for (_before, changes_for_this_before) in grouped {
130 if changes_for_this_before.len() == 1 {
131 // No duplicates, keep as is
132 result.extend(changes_for_this_before);
133 } else {
134 // Multiple changes from the same before state
135 // Check if any of them is to a null state (0: 0 0)
136 let null_link = Link::new(0, 0, 0);
137 let has_null_transition = changes_for_this_before
138 .iter()
139 .any(|(_, after)| *after == null_link);
140 let non_null_transitions: Vec<_> = changes_for_this_before
141 .iter()
142 .filter(|(_, after)| *after != null_link)
143 .cloned()
144 .collect();
145
146 if has_null_transition && !non_null_transitions.is_empty() {
147 // Issue #26 scenario: We have both null and non-null transitions
148 // Prefer the non-null transitions as they represent the actual final states
149 result.extend(non_null_transitions);
150 } else {
151 // No null transitions involved, this is a legitimate multiple-branch scenario
152 // Keep all transitions
153 result.extend(changes_for_this_before);
154 }
155 }
156 }
157
158 result
159}