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 ViolationType::FunctionTooLarge => {
208 effects.push("May require creating new helper functions".to_string());
209 effects.push("Could affect function testing".to_string());
210 }
211 _ => {}
212 }
213
214 effects
215 }
216
217 fn analyze_project_patterns(&self) -> Result<CodePatterns> {
218 let mut all_content = String::new();
219 let mut count = 0;
220
221 fn visit_dir(
222 dir: &std::path::Path,
223 content: &mut String,
224 count: &mut usize,
225 max: usize,
226 ) -> Result<()> {
227 if *count >= max {
228 return Ok(());
229 }
230
231 for entry in fs::read_dir(dir)? {
232 let entry = entry?;
233 let path = entry.path();
234
235 if path.is_dir()
236 && !path
237 .file_name()
238 .unwrap_or_default()
239 .to_string_lossy()
240 .starts_with('.')
241 {
242 visit_dir(&path, content, count, max)?;
243 } else if path.extension().is_some_and(|ext| ext == "rs")
244 && let Ok(file_content) = fs::read_to_string(&path)
245 {
246 content.push_str(&file_content);
247 *count += 1;
248 if *count >= max {
249 break;
250 }
251 }
252 }
253 Ok(())
254 }
255
256 visit_dir(
257 &self.project_root.join("src"),
258 &mut all_content,
259 &mut count,
260 10,
261 )?;
262
263 Ok(identify_code_patterns(&all_content))
264 }
265
266 pub async fn analyze_violations_async(
272 &self,
273 violations: Vec<Violation>,
274 ) -> Result<AIAnalysisReport> {
275 self.analyze_violations(violations)
276 }
277
278 pub fn save_analysis(&self, report: &AIAnalysisReport) -> Result<()> {
285 let analysis_dir = self.project_root.join(".ferrous-forge").join("ai-analysis");
286 fs::create_dir_all(&analysis_dir)?;
287
288 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
289 let filename = format!("ai_analysis_{}.json", timestamp);
290 let filepath = analysis_dir.join(&filename);
291
292 let json = serde_json::to_string_pretty(&report)?;
293 fs::write(&filepath, json)?;
294
295 println!("📊 AI analysis saved to: {}", filepath.display());
296
297 self.save_orchestrator_instructions(report)?;
298
299 Ok(())
300 }
301
302 pub fn save_orchestrator_instructions(&self, report: &AIAnalysisReport) -> Result<()> {
308 let analysis_dir = self.project_root.join(".ferrous-forge").join("ai-analysis");
309 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
310 let filename = format!("orchestrator_instructions_{}.md", timestamp);
311 let filepath = analysis_dir.join(&filename);
312
313 let mut instructions = String::new();
314 instructions.push_str("# AI Orchestrator Instructions\n\n");
315
316 let locked_analyses: Vec<&ViolationAnalysis> = report
318 .violation_analyses
319 .iter()
320 .filter(|a| a.violation.is_locked_setting())
321 .collect();
322
323 if !locked_analyses.is_empty() {
324 instructions.push_str("## Locked Settings (DO NOT MODIFY)\n\n");
325 instructions.push_str(
326 "The following are locked by Ferrous Forge project configuration.\n\
327 DO NOT change these to resolve compilation errors — escalate to human.\n\n",
328 );
329 instructions.push_str("| Setting | Violation | Config |\n");
330 instructions.push_str("|---------|-----------|--------|\n");
331 for a in &locked_analyses {
332 let setting = match a.violation.violation_type {
333 ViolationType::WrongEdition => "edition",
334 ViolationType::OldRustVersion => "rust-version",
335 _ => "locked setting",
336 };
337 instructions.push_str(&format!(
338 "| {} | {} | .ferrous-forge/config.toml |\n",
339 setting,
340 a.violation.file.display()
341 ));
342 }
343 instructions.push_str(
344 "\n**AI Agent Rule**: `ai_fixable = false`, \
345 `confidence = 0%` for all locked violations above.\n\n",
346 );
347 }
348
349 instructions.push_str(&format!(
350 "## Summary\n{}\n\n",
351 report.ai_instructions.summary
352 ));
353
354 instructions.push_str("## Prioritized Fixes\n");
355 for fix in &report.ai_instructions.prioritized_fixes {
356 instructions.push_str(&format!("- {}\n", fix));
357 }
358
359 instructions.push_str("\n## Architectural Recommendations\n");
360 for rec in &report.ai_instructions.architectural_recommendations {
361 instructions.push_str(&format!("- {}\n", rec));
362 }
363
364 fs::write(&filepath, instructions)?;
365 println!(
366 "📝 Orchestrator instructions saved to: {}",
367 filepath.display()
368 );
369
370 Ok(())
371 }
372}