acp/annotate/heuristics/
mod.rs

1//! @acp:module "Annotation Heuristics"
2//! @acp:summary "Pattern-based inference rules for suggesting annotations"
3//! @acp:domain cli
4//! @acp:layer service
5//! @acp:stability experimental
6//!
7//! # Annotation Heuristics
8//!
9//! Provides rule-based inference for suggesting ACP annotations based on:
10//! - Naming patterns (security, data, test keywords)
11//! - Path patterns (directory → domain mapping)
12//! - Visibility patterns (public/private inference)
13//! - Code patterns (security-critical functions)
14//! - Git history (churn, contributors, code age)
15
16pub mod git;
17pub mod naming;
18pub mod path;
19pub mod visibility;
20
21use std::path::Path;
22
23use crate::ast::SymbolKind;
24use crate::git::GitRepository;
25
26use super::{Suggestion, SuggestionSource};
27
28/// @acp:summary "Aggregates heuristic suggestions from multiple sources"
29/// @acp:lock normal
30pub struct HeuristicsEngine {
31    /// Naming pattern heuristics
32    naming: naming::NamingHeuristics,
33
34    /// Path-based heuristics
35    path: path::PathHeuristics,
36
37    /// Visibility-based heuristics (used in future enhancement)
38    #[allow(dead_code)]
39    visibility: visibility::VisibilityHeuristics,
40
41    /// Git-based heuristics
42    git: git::GitHeuristics,
43
44    /// Whether to generate summaries from identifiers
45    generate_summaries: bool,
46
47    /// Whether to use git-based heuristics
48    use_git_heuristics: bool,
49}
50
51impl HeuristicsEngine {
52    /// @acp:summary "Creates a new heuristics engine with default settings"
53    pub fn new() -> Self {
54        Self {
55            naming: naming::NamingHeuristics::new(),
56            path: path::PathHeuristics::new(),
57            visibility: visibility::VisibilityHeuristics::new(),
58            git: git::GitHeuristics::new(),
59            generate_summaries: true,
60            use_git_heuristics: true,
61        }
62    }
63
64    /// @acp:summary "Enables or disables summary generation"
65    pub fn with_summary_generation(mut self, enabled: bool) -> Self {
66        self.generate_summaries = enabled;
67        self
68    }
69
70    /// @acp:summary "Enables or disables git-based heuristics"
71    pub fn with_git_heuristics(mut self, enabled: bool) -> Self {
72        self.use_git_heuristics = enabled;
73        self
74    }
75
76    /// @acp:summary "Generates suggestions for a symbol"
77    ///
78    /// Collects suggestions from all heuristic sources:
79    /// 1. Naming patterns (security, data, test keywords)
80    /// 2. Path patterns (directory → domain)
81    /// 3. Visibility patterns (lock levels)
82    /// 4. Summary generation from identifier names
83    pub fn suggest(
84        &self,
85        target: &str,
86        line: usize,
87        symbol_kind: Option<SymbolKind>,
88        file_path: &str,
89    ) -> Vec<Suggestion> {
90        self.suggest_full(target, line, symbol_kind, file_path, None, false)
91    }
92
93    /// @acp:summary "Generates suggestions for a symbol with visibility info"
94    ///
95    /// Full version that includes visibility-based suggestions.
96    pub fn suggest_full(
97        &self,
98        target: &str,
99        line: usize,
100        symbol_kind: Option<SymbolKind>,
101        file_path: &str,
102        visibility: Option<crate::ast::Visibility>,
103        is_exported: bool,
104    ) -> Vec<Suggestion> {
105        let mut suggestions = Vec::new();
106
107        // Handle file-level targets (symbol_kind = None, target contains path separator)
108        let is_file_level =
109            symbol_kind.is_none() && (target.contains('/') || target.contains('\\'));
110
111        if is_file_level {
112            // Generate @acp:module from file path
113            if let Some(module_name) = self.path.infer_module_name(file_path) {
114                suggestions.push(
115                    Suggestion::module(target, line, &module_name, SuggestionSource::Heuristic)
116                        .with_confidence(0.7),
117                );
118
119                // Generate @acp:summary for the module
120                if self.generate_summaries {
121                    let summary = format!("{} module", module_name);
122                    suggestions.push(
123                        Suggestion::summary(target, line, &summary, SuggestionSource::Heuristic)
124                            .with_confidence(0.5),
125                    );
126                }
127            }
128
129            // Return early - no need for symbol-level heuristics
130            return suggestions;
131        }
132
133        // Get naming-based suggestions
134        let naming_suggestions = self.naming.suggest(target, line);
135        suggestions.extend(naming_suggestions);
136
137        // Get path-based suggestions
138        let path_suggestions = self.path.suggest(file_path, target, line);
139        suggestions.extend(path_suggestions);
140
141        // Get visibility-based suggestions (if we have visibility info)
142        if let Some(vis) = visibility {
143            let visibility_suggestions = self.visibility.suggest(target, line, vis, is_exported);
144            suggestions.extend(visibility_suggestions);
145        }
146
147        // Generate summary from identifier name
148        if self.generate_summaries {
149            if let Some(summary) = self.naming.generate_summary(target, symbol_kind) {
150                suggestions.push(
151                    Suggestion::summary(target, line, summary, SuggestionSource::Heuristic)
152                        .with_confidence(0.6),
153                );
154            }
155        }
156
157        suggestions
158    }
159
160    /// @acp:summary "Generates suggestions including git-based heuristics"
161    ///
162    /// Extended version that also collects git history-based suggestions:
163    /// - High churn detection
164    /// - Single contributor warnings
165    /// - Code stability assessment
166    pub fn suggest_with_git(
167        &self,
168        target: &str,
169        line: usize,
170        symbol_kind: Option<SymbolKind>,
171        file_path: &str,
172        repo: Option<&GitRepository>,
173    ) -> Vec<Suggestion> {
174        self.suggest_with_git_full(target, line, symbol_kind, file_path, repo, None, false)
175    }
176
177    /// @acp:summary "Generates all suggestions including git and visibility"
178    ///
179    /// Full version with all heuristic sources:
180    /// - Naming patterns
181    /// - Path patterns
182    /// - Visibility patterns
183    /// - Git history analysis
184    #[allow(clippy::too_many_arguments)]
185    pub fn suggest_with_git_full(
186        &self,
187        target: &str,
188        line: usize,
189        symbol_kind: Option<SymbolKind>,
190        file_path: &str,
191        repo: Option<&GitRepository>,
192        visibility: Option<crate::ast::Visibility>,
193        is_exported: bool,
194    ) -> Vec<Suggestion> {
195        let mut suggestions = self.suggest_full(
196            target,
197            line,
198            symbol_kind,
199            file_path,
200            visibility,
201            is_exported,
202        );
203
204        // Add git-based suggestions if enabled and repo is available
205        if self.use_git_heuristics {
206            if let Some(repo) = repo {
207                let path = Path::new(file_path);
208                let git_suggestions = self.git.suggest_for_file(repo, path, target, line);
209                suggestions.extend(git_suggestions);
210            }
211        }
212
213        suggestions
214    }
215}
216
217impl Default for HeuristicsEngine {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::annotate::AnnotationType;
227
228    #[test]
229    fn test_heuristics_engine_creation() {
230        let engine = HeuristicsEngine::new();
231        assert!(engine.generate_summaries);
232    }
233
234    #[test]
235    fn test_suggest_security_pattern() {
236        let engine = HeuristicsEngine::new();
237        let suggestions = engine.suggest(
238            "authenticateUser",
239            10,
240            Some(SymbolKind::Function),
241            "src/auth/service.ts",
242        );
243
244        // Should suggest security-related annotations
245        let has_security = suggestions.iter().any(|s| {
246            s.annotation_type == AnnotationType::Domain && s.value.contains("security")
247                || s.annotation_type == AnnotationType::Lock
248        });
249
250        assert!(has_security || !suggestions.is_empty());
251    }
252
253    #[test]
254    fn test_suggest_from_path() {
255        let engine = HeuristicsEngine::new();
256        let suggestions = engine.suggest(
257            "processPayment",
258            10,
259            Some(SymbolKind::Function),
260            "src/billing/payments.ts",
261        );
262
263        // Should suggest billing domain from path
264        let has_billing = suggestions
265            .iter()
266            .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "billing");
267
268        assert!(has_billing);
269    }
270
271    #[test]
272    fn test_suggest_with_visibility_private() {
273        use crate::ast::Visibility;
274
275        let engine = HeuristicsEngine::new();
276        let suggestions = engine.suggest_full(
277            "internalHelper",
278            10,
279            Some(SymbolKind::Function),
280            "src/utils.ts",
281            Some(Visibility::Private),
282            false,
283        );
284
285        // Should suggest restricted lock for private symbols
286        let has_restricted = suggestions
287            .iter()
288            .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "restricted");
289
290        assert!(has_restricted, "Private symbols should get restricted lock");
291    }
292
293    #[test]
294    fn test_suggest_with_visibility_public_exported() {
295        use crate::ast::Visibility;
296
297        let engine = HeuristicsEngine::new();
298        let suggestions = engine.suggest_full(
299            "PublicAPI",
300            10,
301            Some(SymbolKind::Function),
302            "src/api.ts",
303            Some(Visibility::Public),
304            true, // exported
305        );
306
307        // Should suggest normal lock for public exported symbols
308        let has_normal = suggestions
309            .iter()
310            .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "normal");
311
312        assert!(has_normal, "Public exported symbols should get normal lock");
313    }
314
315    #[test]
316    fn test_suggest_full_combines_all_sources() {
317        use crate::ast::Visibility;
318
319        let engine = HeuristicsEngine::new();
320        let suggestions = engine.suggest_full(
321            "authenticateUser",
322            10,
323            Some(SymbolKind::Function),
324            "src/auth/login.ts",
325            Some(Visibility::Internal),
326            false,
327        );
328
329        // Should have suggestions from naming (security), path (auth), and visibility
330        assert!(!suggestions.is_empty(), "Should have combined suggestions");
331
332        // Should have naming-based security suggestion
333        let has_naming = suggestions
334            .iter()
335            .any(|s| s.annotation_type == AnnotationType::Domain);
336        assert!(has_naming, "Should have naming/path-based suggestions");
337
338        // Should have visibility-based suggestions
339        let has_visibility = suggestions.iter().any(|s| {
340            s.annotation_type == AnnotationType::Lock || s.annotation_type == AnnotationType::AiHint
341        });
342        assert!(has_visibility, "Should have visibility-based suggestions");
343    }
344
345    #[test]
346    fn test_git_heuristics_disabled() {
347        let engine = HeuristicsEngine::new().with_git_heuristics(false);
348        assert!(!engine.use_git_heuristics);
349    }
350}