raz_override/
resolver.rs

1use crate::{
2    OverrideEntry,
3    detector::FunctionDetector,
4    error::Result,
5    key::{FunctionContext, OverrideKey},
6    storage::OverrideStorage,
7};
8use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
9
10/// Resolution strategy for finding overrides
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum ResolutionStrategy {
13    /// Exact match on primary key
14    Exact,
15    /// Match on any fallback key
16    Fallback,
17    /// Fuzzy match on function name
18    FuzzyFunction,
19    /// Line-based proximity match
20    LineProximity,
21    /// Hash-based ultimate fallback
22    HashFallback,
23}
24
25/// Resolves overrides using multiple strategies
26pub struct OverrideResolver {
27    storage: OverrideStorage,
28    detector: FunctionDetector,
29    fuzzy_matcher: SkimMatcherV2,
30}
31
32impl OverrideResolver {
33    /// Create a new resolver
34    pub fn new(storage: OverrideStorage) -> Result<Self> {
35        Ok(Self {
36            storage,
37            detector: FunctionDetector::new()?,
38            fuzzy_matcher: SkimMatcherV2::default(),
39        })
40    }
41
42    /// Resolve an override using all available strategies
43    pub fn resolve(
44        &mut self,
45        context: &FunctionContext,
46    ) -> Result<Option<(OverrideEntry, ResolutionStrategy)>> {
47        // Try each strategy in order
48        let strategies = [
49            ResolutionStrategy::Exact,
50            ResolutionStrategy::Fallback,
51            ResolutionStrategy::FuzzyFunction,
52            ResolutionStrategy::LineProximity,
53            ResolutionStrategy::HashFallback,
54        ];
55
56        for strategy in strategies {
57            if let Some(entry) = self.try_resolve_with_strategy(context, strategy)? {
58                log::debug!("Resolved override using strategy: {strategy:?}");
59                return Ok(Some((entry, strategy)));
60            }
61        }
62
63        Ok(None)
64    }
65
66    /// Try to resolve with a specific strategy
67    fn try_resolve_with_strategy(
68        &mut self,
69        context: &FunctionContext,
70        strategy: ResolutionStrategy,
71    ) -> Result<Option<OverrideEntry>> {
72        match strategy {
73            ResolutionStrategy::Exact => self.resolve_exact(context),
74            ResolutionStrategy::Fallback => self.resolve_fallback(context),
75            ResolutionStrategy::FuzzyFunction => self.resolve_fuzzy_function(context),
76            ResolutionStrategy::LineProximity => self.resolve_line_proximity(context),
77            ResolutionStrategy::HashFallback => self.resolve_hash_fallback(context),
78        }
79    }
80
81    /// Try exact match on primary key
82    fn resolve_exact(&self, context: &FunctionContext) -> Result<Option<OverrideEntry>> {
83        let key = OverrideKey::new(context)?;
84        self.storage.get_by_primary_key(&key.primary)
85    }
86
87    /// Try fallback keys
88    fn resolve_fallback(&self, context: &FunctionContext) -> Result<Option<OverrideEntry>> {
89        let key = OverrideKey::new(context)?;
90
91        for fallback in &key.fallbacks {
92            if let Some(entry) = self.storage.get_by_key(fallback)? {
93                return Ok(Some(entry));
94            }
95        }
96
97        Ok(None)
98    }
99
100    /// Try fuzzy matching on function name
101    fn resolve_fuzzy_function(
102        &mut self,
103        context: &FunctionContext,
104    ) -> Result<Option<OverrideEntry>> {
105        // Need a function name for fuzzy matching
106        let func_name = match &context.function_name {
107            Some(name) => name,
108            None => return Ok(None),
109        };
110
111        // Get all overrides for this file
112        let file_overrides = self.storage.get_by_file(&context.file_path)?;
113
114        // Find best fuzzy match
115        let mut best_match = None;
116        let mut best_score = 0;
117
118        for entry in file_overrides {
119            if let Some(stored_func) = &entry.metadata.function_name {
120                if let Some(score) = self.fuzzy_matcher.fuzzy_match(stored_func, func_name) {
121                    if score > best_score {
122                        best_score = score;
123                        best_match = Some(entry);
124                    }
125                }
126            }
127        }
128
129        // Only accept good matches (threshold)
130        if best_score > 50 {
131            Ok(best_match)
132        } else {
133            Ok(None)
134        }
135    }
136
137    /// Try to find override by line proximity
138    fn resolve_line_proximity(
139        &mut self,
140        context: &FunctionContext,
141    ) -> Result<Option<OverrideEntry>> {
142        // Read the source file
143        let source = match std::fs::read_to_string(&context.file_path) {
144            Ok(s) => s,
145            Err(_) => return Ok(None),
146        };
147
148        // Find function at the current line
149        let current_func = match self
150            .detector
151            .find_function_at_line(&source, context.line_number)?
152        {
153            Some(f) => f,
154            None => return Ok(None),
155        };
156
157        // Look for overrides with matching function name
158        let file_overrides = self.storage.get_by_file(&context.file_path)?;
159
160        for entry in file_overrides {
161            if let Some(stored_func) = &entry.metadata.function_name {
162                if stored_func == &current_func.name {
163                    return Ok(Some(entry));
164                }
165            }
166        }
167
168        Ok(None)
169    }
170
171    /// Ultimate fallback using hash
172    fn resolve_hash_fallback(&self, context: &FunctionContext) -> Result<Option<OverrideEntry>> {
173        let key = OverrideKey::new(context)?;
174
175        // Find the hash key in fallbacks
176        if let Some(hash_key) = key.fallbacks.iter().find(|k| k.starts_with("hash:")) {
177            return self.storage.get_by_key(hash_key);
178        }
179
180        Ok(None)
181    }
182
183    /// Get resolution candidates for debugging
184    pub fn get_resolution_candidates(
185        &mut self,
186        context: &FunctionContext,
187    ) -> Result<Vec<(OverrideEntry, ResolutionStrategy, f32)>> {
188        let mut candidates = Vec::new();
189
190        // Check each strategy
191        for strategy in [
192            ResolutionStrategy::Exact,
193            ResolutionStrategy::Fallback,
194            ResolutionStrategy::FuzzyFunction,
195            ResolutionStrategy::LineProximity,
196            ResolutionStrategy::HashFallback,
197        ] {
198            if let Some(entry) = self.try_resolve_with_strategy(context, strategy)? {
199                let confidence = match strategy {
200                    ResolutionStrategy::Exact => 1.0,
201                    ResolutionStrategy::Fallback => 0.9,
202                    ResolutionStrategy::FuzzyFunction => 0.7,
203                    ResolutionStrategy::LineProximity => 0.6,
204                    ResolutionStrategy::HashFallback => 0.5,
205                };
206
207                candidates.push((entry, strategy, confidence));
208            }
209        }
210
211        Ok(candidates)
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::OverrideMetadata;
219    use raz_config::CommandOverride;
220    use std::path::PathBuf;
221    use tempfile::TempDir;
222
223    #[test]
224    fn test_exact_resolution() {
225        let temp_dir = TempDir::new().unwrap();
226        let storage = OverrideStorage::new(temp_dir.path()).unwrap();
227        let mut resolver = OverrideResolver::new(storage).unwrap();
228
229        // Create and save an override
230        let context = FunctionContext {
231            file_path: PathBuf::from("src/main.rs"),
232            function_name: Some("test_func".to_string()),
233            line_number: 10,
234            context: None,
235        };
236
237        let key = OverrideKey::new(&context).unwrap();
238        let override_config = CommandOverride::new("test".to_string());
239        let entry = OverrideEntry {
240            key: key.clone(),
241            override_config,
242            metadata: OverrideMetadata {
243                created_at: chrono::Utc::now(),
244                modified_at: chrono::Utc::now(),
245                file_path: context.file_path.clone(),
246                function_name: context.function_name.clone(),
247                original_line: Some(context.line_number),
248                notes: None,
249                validation_status: crate::ValidationStatus::Pending,
250                failure_count: 0,
251                last_execution_success: None,
252                last_execution_time: None,
253            },
254        };
255
256        resolver.storage.save(&entry).unwrap();
257
258        // Resolve should find it with exact strategy
259        let result = resolver.resolve(&context).unwrap();
260        assert!(result.is_some());
261
262        let (resolved_entry, strategy) = result.unwrap();
263        assert_eq!(strategy, ResolutionStrategy::Exact);
264        assert_eq!(resolved_entry.key.primary, key.primary);
265    }
266}