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