Skip to main content

merge_engine/
resolver.rs

1//! Main conflict resolver pipeline.
2//!
3//! Orchestrates the multi-strategy resolution approach, applying techniques
4//! in order of confidence from highest to lowest:
5//!
6//! 1. **Pattern rules** (DSL) — highest confidence, instant (ICSE 2021)
7//! 2. **Structured merge** (tree-level) — eliminates false conflicts (LASTMERGE 2025)
8//! 3. **Version Space Algebra** — enumerates combinations (OOPSLA 2018)
9//! 4. **Search-based** — evolutionary with parent similarity (TOSEM 2025)
10//!
11//! The resolver stops at the first strategy that produces a resolution with
12//! sufficient confidence, or returns ranked candidates from all strategies.
13
14use crate::amalgamator::{AmalgamResult, amalgam_to_merge_result, amalgamate};
15use crate::diff3;
16use crate::parser::{self, ParseError};
17use crate::patterns::PatternRegistry;
18use crate::search::{self, SearchConfig};
19use crate::types::*;
20use crate::vsa;
21
22/// Configuration for the resolver pipeline.
23pub struct ResolverConfig {
24    /// Minimum confidence to auto-accept a resolution.
25    pub auto_accept_threshold: Confidence,
26    /// Maximum VSA candidates to enumerate.
27    pub max_vsa_candidates: usize,
28    /// Search-based resolver configuration.
29    pub search_config: SearchConfig,
30    /// Programming language (for structured merge). None = text-only mode.
31    pub language: Option<Language>,
32}
33
34impl Default for ResolverConfig {
35    fn default() -> Self {
36        Self {
37            auto_accept_threshold: Confidence::Medium,
38            max_vsa_candidates: 100,
39            search_config: SearchConfig::default(),
40            language: None,
41        }
42    }
43}
44
45/// The main resolver that combines all strategies.
46pub struct Resolver {
47    config: ResolverConfig,
48    patterns: PatternRegistry,
49}
50
51/// Result of the full resolution pipeline.
52#[derive(Debug)]
53pub struct ResolverOutput {
54    /// The best resolution, if any strategy produced one above threshold.
55    pub resolution: Option<ResolutionCandidate>,
56    /// All candidate resolutions, ranked by confidence.
57    pub candidates: Vec<ResolutionCandidate>,
58    /// Which strategies were attempted.
59    pub strategies_tried: Vec<ResolutionStrategy>,
60    /// The original merge result (possibly a conflict).
61    pub diff3_result: MergeResult,
62}
63
64impl Resolver {
65    pub fn new(config: ResolverConfig) -> Self {
66        Self {
67            config,
68            patterns: PatternRegistry::new(),
69        }
70    }
71
72    /// Resolve a three-way merge for complete file contents.
73    ///
74    /// This is the main entry point. It first runs diff3 to identify conflict
75    /// regions, then applies the resolution pipeline to each conflict.
76    pub fn resolve_file(&self, base: &str, left: &str, right: &str) -> FileResolverOutput {
77        let scenario = MergeScenario::new(base, left, right);
78        let diff3_result = diff3::diff3_merge(&scenario);
79
80        match &diff3_result {
81            MergeResult::Resolved(content) => {
82                // No conflicts — diff3 handled it
83                FileResolverOutput {
84                    merged_content: content.clone(),
85                    conflicts: vec![],
86                    all_resolved: true,
87                }
88            }
89            MergeResult::Conflict { .. } => {
90                // Extract individual conflict regions and resolve each
91                let _conflicts = diff3::extract_conflicts(&scenario);
92                let mut merged_parts = Vec::new();
93                let mut unresolved = Vec::new();
94                let mut all_resolved = true;
95
96                // Re-run diff3 to get hunks for reconstruction
97                let hunks = diff3::diff3_hunks(&scenario);
98
99                for hunk in &hunks {
100                    match hunk {
101                        Diff3Hunk::Stable(lines)
102                        | Diff3Hunk::LeftChanged(lines)
103                        | Diff3Hunk::RightChanged(lines) => {
104                            for line in lines {
105                                merged_parts.push(line.clone());
106                            }
107                        }
108                        Diff3Hunk::Conflict { base, left, right } => {
109                            let conflict_scenario = MergeScenario::new(
110                                base.join("\n").as_str().to_string(),
111                                left.join("\n").as_str().to_string(),
112                                right.join("\n").as_str().to_string(),
113                            );
114
115                            let output = self.resolve_conflict(
116                                &conflict_scenario.base,
117                                &conflict_scenario.left,
118                                &conflict_scenario.right,
119                            );
120
121                            if let Some(ref resolution) = output.resolution {
122                                for line in resolution.content.lines() {
123                                    merged_parts.push(line.to_string());
124                                }
125                            } else {
126                                all_resolved = false;
127                                // Insert conflict markers
128                                merged_parts.push("<<<<<<< LEFT".to_string());
129                                merged_parts.extend(left.iter().cloned());
130                                merged_parts.push("||||||| BASE".to_string());
131                                merged_parts.extend(base.iter().cloned());
132                                merged_parts.push("=======".to_string());
133                                merged_parts.extend(right.iter().cloned());
134                                merged_parts.push(">>>>>>> RIGHT".to_string());
135                            }
136                            unresolved.push(output);
137                        }
138                    }
139                }
140
141                FileResolverOutput {
142                    merged_content: merged_parts.join("\n"),
143                    conflicts: unresolved,
144                    all_resolved,
145                }
146            }
147        }
148    }
149
150    /// Resolve a single conflict region using the full pipeline.
151    pub fn resolve_conflict(&self, base: &str, left: &str, right: &str) -> ResolverOutput {
152        let mut candidates: Vec<ResolutionCandidate> = Vec::new();
153        let mut strategies_tried = Vec::new();
154
155        let text_scenario = MergeScenario::new(base, left, right);
156        let diff3_result = diff3::diff3_merge(&text_scenario);
157
158        // ── Strategy 1: Pattern-based DSL rules ──
159        strategies_tried.push(ResolutionStrategy::PatternRule);
160        if let Some(resolution) = self.patterns.try_resolve(&text_scenario) {
161            if resolution.confidence >= self.config.auto_accept_threshold {
162                return ResolverOutput {
163                    resolution: Some(resolution.clone()),
164                    candidates: vec![resolution],
165                    strategies_tried,
166                    diff3_result,
167                };
168            }
169            candidates.push(resolution);
170        }
171
172        // ── Strategy 2: Structured tree merge ──
173        if let Some(lang) = self.config.language {
174            strategies_tried.push(ResolutionStrategy::StructuredMerge);
175            match self.try_structured_merge(base, left, right, lang) {
176                Ok(Some(result)) => {
177                    if let MergeResult::Resolved(content) = result {
178                        let resolution = ResolutionCandidate {
179                            content,
180                            confidence: Confidence::High,
181                            strategy: ResolutionStrategy::StructuredMerge,
182                        };
183                        if resolution.confidence >= self.config.auto_accept_threshold {
184                            return ResolverOutput {
185                                resolution: Some(resolution.clone()),
186                                candidates: vec![resolution],
187                                strategies_tried,
188                                diff3_result,
189                            };
190                        }
191                        candidates.push(resolution);
192                    }
193                }
194                Ok(None) => {} // Structured merge also found a conflict
195                Err(_) => {}   // Parse error — skip this strategy
196            }
197        }
198
199        // ── Strategy 3: Version Space Algebra ──
200        if let Some(lang) = self.config.language {
201            strategies_tried.push(ResolutionStrategy::VersionSpaceAlgebra);
202            if let Ok(vsa_candidates) = self.try_vsa_resolve(base, left, right, lang) {
203                candidates.extend(vsa_candidates);
204            }
205        }
206
207        // ── Strategy 4: Search-based resolution ──
208        strategies_tried.push(ResolutionStrategy::SearchBased);
209        let search_candidates = search::search_resolve(&text_scenario, &self.config.search_config);
210        candidates.extend(search_candidates);
211
212        // Sort all candidates by confidence
213        candidates.sort_by(|a, b| b.confidence.cmp(&a.confidence));
214
215        // Deduplicate by content
216        let mut seen = std::collections::HashSet::new();
217        candidates.retain(|c| seen.insert(c.content.clone()));
218
219        let resolution = candidates
220            .first()
221            .filter(|c| c.confidence >= self.config.auto_accept_threshold)
222            .cloned();
223
224        ResolverOutput {
225            resolution,
226            candidates,
227            strategies_tried,
228            diff3_result,
229        }
230    }
231
232    /// Attempt structured tree merge for a conflict region.
233    fn try_structured_merge(
234        &self,
235        base: &str,
236        left: &str,
237        right: &str,
238        lang: Language,
239    ) -> Result<Option<MergeResult>, ParseError> {
240        let base_tree = parser::parse_to_cst(base, lang)?;
241        let left_tree = parser::parse_to_cst(left, lang)?;
242        let right_tree = parser::parse_to_cst(right, lang)?;
243
244        let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
245        let result = amalgamate(&scenario);
246
247        match result {
248            AmalgamResult::Merged(_) => Ok(Some(amalgam_to_merge_result(&result))),
249            AmalgamResult::Conflict { .. } => Ok(None),
250        }
251    }
252
253    /// Attempt VSA resolution for a conflict region.
254    fn try_vsa_resolve(
255        &self,
256        base: &str,
257        left: &str,
258        right: &str,
259        lang: Language,
260    ) -> Result<Vec<ResolutionCandidate>, ParseError> {
261        let base_tree = parser::parse_to_cst(base, lang)?;
262        let left_tree = parser::parse_to_cst(left, lang)?;
263        let right_tree = parser::parse_to_cst(right, lang)?;
264
265        let scenario = MergeScenario::new(&base_tree, &left_tree, &right_tree);
266        Ok(vsa::resolve_via_vsa(
267            &scenario,
268            self.config.max_vsa_candidates,
269        ))
270    }
271}
272
273/// Output of resolving a complete file.
274#[derive(Debug)]
275pub struct FileResolverOutput {
276    /// The merged file content (may contain conflict markers if not fully resolved).
277    pub merged_content: String,
278    /// Per-conflict resolution details.
279    pub conflicts: Vec<ResolverOutput>,
280    /// Whether all conflicts were resolved.
281    pub all_resolved: bool,
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_clean_merge() {
290        let resolver = Resolver::new(ResolverConfig::default());
291        let result = resolver.resolve_file(
292            "line1\nline2\nline3\n",
293            "lineA\nline2\nline3\n",
294            "line1\nline2\nlineC\n",
295        );
296        assert!(result.all_resolved);
297    }
298
299    #[test]
300    fn test_pattern_resolves_whitespace() {
301        let resolver = Resolver::new(ResolverConfig::default());
302        let output = resolver.resolve_conflict("int x = 1;", "int  x = 1;", "int x  = 1;");
303        assert!(output.resolution.is_some());
304        assert_eq!(
305            output.resolution.unwrap().strategy,
306            ResolutionStrategy::PatternRule
307        );
308    }
309
310    #[test]
311    fn test_search_fallback() {
312        let resolver = Resolver::new(ResolverConfig::default());
313        let output = resolver.resolve_conflict(
314            "fn foo() { return 1; }",
315            "fn foo() { return 2; }",
316            "fn bar() { return 1; }",
317        );
318        // Should have candidates even for hard conflicts
319        assert!(!output.candidates.is_empty());
320    }
321
322    #[test]
323    fn test_structured_merge_rust() {
324        let config = ResolverConfig {
325            language: Some(Language::Rust),
326            ..Default::default()
327        };
328        let resolver = Resolver::new(config);
329        let output = resolver.resolve_conflict(
330            "fn main() { let x = 1; }",
331            "fn main() { let x = 2; }",
332            "fn main() { let x = 1; let y = 3; }",
333        );
334        // Should attempt structured merge
335        assert!(
336            output
337                .strategies_tried
338                .contains(&ResolutionStrategy::StructuredMerge)
339        );
340    }
341}