Skip to main content

ferrous_forge/ai_analyzer/
analyzer.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::Result;
5use chrono::Utc;
6
7use super::context::extract_code_context;
8use super::semantic::{assess_fix_complexity, perform_semantic_analysis};
9use super::strategies::{
10    generate_ai_instructions, generate_fix_strategies, identify_code_patterns,
11};
12use super::types::*;
13use crate::validation::{Violation, ViolationType};
14
15/// AI analyzer for automated violation analysis
16pub struct AIAnalyzer {
17    project_root: PathBuf,
18}
19
20impl AIAnalyzer {
21    /// Create a new AI analyzer
22    pub fn new(project_root: PathBuf) -> Self {
23        Self { project_root }
24    }
25
26    /// Analyze violations and generate report
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if reading source files or analyzing project patterns fails.
31    pub fn analyze_violations(&self, violations: Vec<Violation>) -> Result<AIAnalysisReport> {
32        let mut violation_analyses = Vec::new();
33        let mut analyzable_count = 0;
34
35        for violation in &violations {
36            if let Ok(analysis) = self.analyze_single_violation(violation) {
37                if analysis.ai_fixable {
38                    analyzable_count += 1;
39                }
40                violation_analyses.push(analysis);
41            }
42        }
43
44        let code_patterns = self.analyze_project_patterns()?;
45        let fix_strategies = generate_fix_strategies(&violation_analyses);
46        let ai_instructions = generate_ai_instructions(&violation_analyses, &fix_strategies);
47
48        let metadata = AnalysisMetadata {
49            total_violations: violations.len(),
50            analyzable_violations: analyzable_count,
51            project_path: self.project_root.display().to_string(),
52            analysis_depth: AnalysisDepth::Semantic,
53        };
54
55        Ok(AIAnalysisReport {
56            metadata,
57            violation_analyses,
58            code_patterns,
59            fix_strategies,
60            ai_instructions,
61        })
62    }
63
64    fn analyze_single_violation(&self, violation: &Violation) -> Result<ViolationAnalysis> {
65        // Locked settings are never AI-fixable — return early with explicit guidance
66        if violation.is_locked_setting() {
67            return Ok(self.build_locked_analysis(violation));
68        }
69
70        let content = fs::read_to_string(&violation.file)?;
71        let code_context = extract_code_context(violation.line, &content);
72        let semantic_analysis = perform_semantic_analysis(violation, &code_context, &content);
73        let fix_complexity = assess_fix_complexity(violation, &code_context, &semantic_analysis);
74
75        let (ai_fixable, confidence_score) = self.assess_fixability(
76            violation,
77            &code_context,
78            &semantic_analysis,
79            &fix_complexity,
80        );
81
82        let fix_recommendation = if ai_fixable {
83            self.generate_fix_recommendation(violation, &code_context, &semantic_analysis)
84        } else {
85            None
86        };
87
88        let side_effects = self.identify_side_effects(violation, &code_context);
89
90        Ok(ViolationAnalysis {
91            violation: violation.clone(),
92            code_context,
93            semantic_analysis,
94            fix_complexity,
95            ai_fixable,
96            fix_recommendation,
97            side_effects,
98            confidence_score,
99        })
100    }
101
102    /// Build a [`ViolationAnalysis`] for locked settings (edition, rust-version)
103    fn build_locked_analysis(&self, violation: &Violation) -> ViolationAnalysis {
104        ViolationAnalysis {
105            violation: violation.clone(),
106            code_context: CodeContext {
107                function_name: None,
108                function_signature: None,
109                return_type: None,
110                is_async: false,
111                is_generic: false,
112                trait_impl: None,
113                surrounding_code: vec![],
114                imports: vec![],
115                error_handling_style: ErrorHandlingStyle::Unknown,
116            },
117            semantic_analysis: super::semantic::empty_semantic_analysis(),
118            fix_complexity: FixComplexity::Architectural,
119            ai_fixable: false,
120            fix_recommendation: Some(
121                "DO NOT change edition or rust-version in Cargo.toml.\n\
122                 These are locked by .ferrous-forge/config.toml.\n\
123                 This violation requires human intervention — escalate to the project owner."
124                    .to_string(),
125            ),
126            side_effects: vec![
127                "Changing locked settings may break CI, team standards, and edition guarantees."
128                    .to_string(),
129            ],
130            confidence_score: 0.0,
131        }
132    }
133
134    fn assess_fixability(
135        &self,
136        violation: &Violation,
137        context: &CodeContext,
138        _semantic: &SemanticAnalysis,
139        complexity: &FixComplexity,
140    ) -> (bool, f32) {
141        match (&violation.violation_type, complexity) {
142            (ViolationType::UnwrapInProduction, FixComplexity::Trivial) => {
143                if context
144                    .return_type
145                    .as_ref()
146                    .is_some_and(|r| r.contains("Result"))
147                {
148                    (true, 0.95)
149                } else {
150                    (true, 0.75)
151                }
152            }
153            (ViolationType::UnwrapInProduction, FixComplexity::Simple) => (true, 0.65),
154            (ViolationType::LineTooLong, _) => (true, 1.0),
155            (ViolationType::UnderscoreBandaid, _) => (true, 0.85),
156            (ViolationType::FunctionTooLarge, _) => (false, 0.3),
157            (ViolationType::FileTooLarge, _) => (false, 0.2),
158            // Locked settings are never AI-fixable (handled in build_locked_analysis)
159            (ViolationType::WrongEdition, _)
160            | (ViolationType::OldRustVersion, _)
161            | (ViolationType::LockedSetting, _) => (false, 0.0),
162            _ => (false, 0.0),
163        }
164    }
165
166    fn generate_fix_recommendation(
167        &self,
168        violation: &Violation,
169        context: &CodeContext,
170        _semantic: &SemanticAnalysis,
171    ) -> Option<String> {
172        match violation.violation_type {
173            ViolationType::UnwrapInProduction => {
174                if context
175                    .return_type
176                    .as_ref()
177                    .is_some_and(|r| r.contains("Result"))
178                {
179                    Some("Replace ? with ? operator".to_string())
180                } else {
181                    Some("Change function return type to Result and use ?".to_string())
182                }
183            }
184            ViolationType::LineTooLong => {
185                Some("Break line at appropriate point (e.g., after comma, operator)".to_string())
186            }
187            ViolationType::UnderscoreBandaid => {
188                Some("Either use the parameter or remove it from function signature".to_string())
189            }
190            _ => None,
191        }
192    }
193
194    fn identify_side_effects(&self, violation: &Violation, context: &CodeContext) -> Vec<String> {
195        let mut effects = Vec::new();
196
197        match violation.violation_type {
198            ViolationType::UnwrapInProduction => {
199                if !context
200                    .return_type
201                    .as_ref()
202                    .is_some_and(|r| r.contains("Result"))
203                {
204                    effects.push("Function signature change required".to_string());
205                    effects.push("All callers must be updated".to_string());
206                }
207            }
208            ViolationType::FunctionTooLarge => {
209                effects.push("May require creating new helper functions".to_string());
210                effects.push("Could affect function testing".to_string());
211            }
212            _ => {}
213        }
214
215        effects
216    }
217
218    fn analyze_project_patterns(&self) -> Result<CodePatterns> {
219        let mut all_content = String::new();
220        let mut count = 0;
221
222        fn visit_dir(
223            dir: &std::path::Path,
224            content: &mut String,
225            count: &mut usize,
226            max: usize,
227        ) -> Result<()> {
228            if *count >= max {
229                return Ok(());
230            }
231
232            for entry in fs::read_dir(dir)? {
233                let entry = entry?;
234                let path = entry.path();
235
236                if path.is_dir()
237                    && !path
238                        .file_name()
239                        .unwrap_or_default()
240                        .to_string_lossy()
241                        .starts_with('.')
242                {
243                    visit_dir(&path, content, count, max)?;
244                } else if path.extension().is_some_and(|ext| ext == "rs")
245                    && let Ok(file_content) = fs::read_to_string(&path)
246                {
247                    content.push_str(&file_content);
248                    *count += 1;
249                    if *count >= max {
250                        break;
251                    }
252                }
253            }
254            Ok(())
255        }
256
257        visit_dir(
258            &self.project_root.join("src"),
259            &mut all_content,
260            &mut count,
261            10,
262        )?;
263
264        Ok(identify_code_patterns(&all_content))
265    }
266
267    /// Async version of [`analyze_violations`](Self::analyze_violations)
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if reading source files or analyzing project patterns fails.
272    pub async fn analyze_violations_async(
273        &self,
274        violations: Vec<Violation>,
275    ) -> Result<AIAnalysisReport> {
276        self.analyze_violations(violations)
277    }
278
279    /// Save analysis report to disk
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if creating the analysis directory, serializing the report,
284    /// or writing files fails.
285    pub fn save_analysis(&self, report: &AIAnalysisReport) -> Result<()> {
286        let analysis_dir = self.project_root.join(".ferrous-forge").join("ai-analysis");
287        fs::create_dir_all(&analysis_dir)?;
288
289        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
290        let filename = format!("ai_analysis_{}.json", timestamp);
291        let filepath = analysis_dir.join(&filename);
292
293        let json = serde_json::to_string_pretty(&report)?;
294        fs::write(&filepath, json)?;
295
296        println!("📊 AI analysis saved to: {}", filepath.display());
297
298        self.save_orchestrator_instructions(report)?;
299
300        Ok(())
301    }
302
303    /// Save orchestrator instructions to file, including locked settings section
304    ///
305    /// # Errors
306    ///
307    /// Returns an error if writing the instructions file fails.
308    pub fn save_orchestrator_instructions(&self, report: &AIAnalysisReport) -> Result<()> {
309        let analysis_dir = self.project_root.join(".ferrous-forge").join("ai-analysis");
310        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
311        let filename = format!("orchestrator_instructions_{}.md", timestamp);
312        let filepath = analysis_dir.join(&filename);
313
314        let mut instructions = String::new();
315        instructions.push_str("# AI Orchestrator Instructions\n\n");
316
317        // Locked settings section — appears first so agents see it immediately
318        let locked_analyses: Vec<&ViolationAnalysis> = report
319            .violation_analyses
320            .iter()
321            .filter(|a| a.violation.is_locked_setting())
322            .collect();
323
324        if !locked_analyses.is_empty() {
325            instructions.push_str("## Locked Settings (DO NOT MODIFY)\n\n");
326            instructions.push_str(
327                "The following are locked by Ferrous Forge project configuration.\n\
328                 DO NOT change these to resolve compilation errors — escalate to human.\n\n",
329            );
330            instructions.push_str("| Setting | Violation | Config |\n");
331            instructions.push_str("|---------|-----------|--------|\n");
332            for a in &locked_analyses {
333                let setting = match a.violation.violation_type {
334                    ViolationType::WrongEdition => "edition",
335                    ViolationType::OldRustVersion => "rust-version",
336                    _ => "locked setting",
337                };
338                instructions.push_str(&format!(
339                    "| {} | {} | .ferrous-forge/config.toml |\n",
340                    setting,
341                    a.violation.file.display()
342                ));
343            }
344            instructions.push_str(
345                "\n**AI Agent Rule**: `ai_fixable = false`, \
346                 `confidence = 0%` for all locked violations above.\n\n",
347            );
348        }
349
350        instructions.push_str(&format!(
351            "## Summary\n{}\n\n",
352            report.ai_instructions.summary
353        ));
354
355        instructions.push_str("## Prioritized Fixes\n");
356        for fix in &report.ai_instructions.prioritized_fixes {
357            instructions.push_str(&format!("- {}\n", fix));
358        }
359
360        instructions.push_str("\n## Architectural Recommendations\n");
361        for rec in &report.ai_instructions.architectural_recommendations {
362            instructions.push_str(&format!("- {}\n", rec));
363        }
364
365        fs::write(&filepath, instructions)?;
366        println!(
367            "📝 Orchestrator instructions saved to: {}",
368            filepath.display()
369        );
370
371        Ok(())
372    }
373}