ferrous_forge/ai_analyzer/
analyzer.rs1use 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
15pub struct AIAnalyzer {
17 project_root: PathBuf,
18}
19
20impl AIAnalyzer {
21 pub fn new(project_root: PathBuf) -> Self {
23 Self { project_root }
24 }
25
26 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 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 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 (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 pub async fn analyze_violations_async(
273 &self,
274 violations: Vec<Violation>,
275 ) -> Result<AIAnalysisReport> {
276 self.analyze_violations(violations)
277 }
278
279 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 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 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}