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        // Get naming-based suggestions
108        let naming_suggestions = self.naming.suggest(target, line);
109        suggestions.extend(naming_suggestions);
110
111        // Get path-based suggestions
112        let path_suggestions = self.path.suggest(file_path, target, line);
113        suggestions.extend(path_suggestions);
114
115        // Get visibility-based suggestions (if we have visibility info)
116        if let Some(vis) = visibility {
117            let visibility_suggestions = self.visibility.suggest(target, line, vis, is_exported);
118            suggestions.extend(visibility_suggestions);
119        }
120
121        // Generate summary from identifier name
122        if self.generate_summaries {
123            if let Some(summary) = self.naming.generate_summary(target, symbol_kind) {
124                suggestions.push(
125                    Suggestion::summary(target, line, summary, SuggestionSource::Heuristic)
126                        .with_confidence(0.6),
127                );
128            }
129        }
130
131        suggestions
132    }
133
134    /// @acp:summary "Generates suggestions including git-based heuristics"
135    ///
136    /// Extended version that also collects git history-based suggestions:
137    /// - High churn detection
138    /// - Single contributor warnings
139    /// - Code stability assessment
140    pub fn suggest_with_git(
141        &self,
142        target: &str,
143        line: usize,
144        symbol_kind: Option<SymbolKind>,
145        file_path: &str,
146        repo: Option<&GitRepository>,
147    ) -> Vec<Suggestion> {
148        self.suggest_with_git_full(target, line, symbol_kind, file_path, repo, None, false)
149    }
150
151    /// @acp:summary "Generates all suggestions including git and visibility"
152    ///
153    /// Full version with all heuristic sources:
154    /// - Naming patterns
155    /// - Path patterns
156    /// - Visibility patterns
157    /// - Git history analysis
158    #[allow(clippy::too_many_arguments)]
159    pub fn suggest_with_git_full(
160        &self,
161        target: &str,
162        line: usize,
163        symbol_kind: Option<SymbolKind>,
164        file_path: &str,
165        repo: Option<&GitRepository>,
166        visibility: Option<crate::ast::Visibility>,
167        is_exported: bool,
168    ) -> Vec<Suggestion> {
169        let mut suggestions = self.suggest_full(
170            target,
171            line,
172            symbol_kind,
173            file_path,
174            visibility,
175            is_exported,
176        );
177
178        // Add git-based suggestions if enabled and repo is available
179        if self.use_git_heuristics {
180            if let Some(repo) = repo {
181                let path = Path::new(file_path);
182                let git_suggestions = self.git.suggest_for_file(repo, path, target, line);
183                suggestions.extend(git_suggestions);
184            }
185        }
186
187        suggestions
188    }
189}
190
191impl Default for HeuristicsEngine {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::annotate::AnnotationType;
201
202    #[test]
203    fn test_heuristics_engine_creation() {
204        let engine = HeuristicsEngine::new();
205        assert!(engine.generate_summaries);
206    }
207
208    #[test]
209    fn test_suggest_security_pattern() {
210        let engine = HeuristicsEngine::new();
211        let suggestions = engine.suggest(
212            "authenticateUser",
213            10,
214            Some(SymbolKind::Function),
215            "src/auth/service.ts",
216        );
217
218        // Should suggest security-related annotations
219        let has_security = suggestions.iter().any(|s| {
220            s.annotation_type == AnnotationType::Domain && s.value.contains("security")
221                || s.annotation_type == AnnotationType::Lock
222        });
223
224        assert!(has_security || !suggestions.is_empty());
225    }
226
227    #[test]
228    fn test_suggest_from_path() {
229        let engine = HeuristicsEngine::new();
230        let suggestions = engine.suggest(
231            "processPayment",
232            10,
233            Some(SymbolKind::Function),
234            "src/billing/payments.ts",
235        );
236
237        // Should suggest billing domain from path
238        let has_billing = suggestions
239            .iter()
240            .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "billing");
241
242        assert!(has_billing);
243    }
244
245    #[test]
246    fn test_suggest_with_visibility_private() {
247        use crate::ast::Visibility;
248
249        let engine = HeuristicsEngine::new();
250        let suggestions = engine.suggest_full(
251            "internalHelper",
252            10,
253            Some(SymbolKind::Function),
254            "src/utils.ts",
255            Some(Visibility::Private),
256            false,
257        );
258
259        // Should suggest restricted lock for private symbols
260        let has_restricted = suggestions
261            .iter()
262            .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "restricted");
263
264        assert!(has_restricted, "Private symbols should get restricted lock");
265    }
266
267    #[test]
268    fn test_suggest_with_visibility_public_exported() {
269        use crate::ast::Visibility;
270
271        let engine = HeuristicsEngine::new();
272        let suggestions = engine.suggest_full(
273            "PublicAPI",
274            10,
275            Some(SymbolKind::Function),
276            "src/api.ts",
277            Some(Visibility::Public),
278            true, // exported
279        );
280
281        // Should suggest normal lock for public exported symbols
282        let has_normal = suggestions
283            .iter()
284            .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "normal");
285
286        assert!(has_normal, "Public exported symbols should get normal lock");
287    }
288
289    #[test]
290    fn test_suggest_full_combines_all_sources() {
291        use crate::ast::Visibility;
292
293        let engine = HeuristicsEngine::new();
294        let suggestions = engine.suggest_full(
295            "authenticateUser",
296            10,
297            Some(SymbolKind::Function),
298            "src/auth/login.ts",
299            Some(Visibility::Internal),
300            false,
301        );
302
303        // Should have suggestions from naming (security), path (auth), and visibility
304        assert!(!suggestions.is_empty(), "Should have combined suggestions");
305
306        // Should have naming-based security suggestion
307        let has_naming = suggestions
308            .iter()
309            .any(|s| s.annotation_type == AnnotationType::Domain);
310        assert!(has_naming, "Should have naming/path-based suggestions");
311
312        // Should have visibility-based suggestions
313        let has_visibility = suggestions.iter().any(|s| {
314            s.annotation_type == AnnotationType::Lock || s.annotation_type == AnnotationType::AiHint
315        });
316        assert!(has_visibility, "Should have visibility-based suggestions");
317    }
318
319    #[test]
320    fn test_git_heuristics_disabled() {
321        let engine = HeuristicsEngine::new().with_git_heuristics(false);
322        assert!(!engine.use_git_heuristics);
323    }
324}