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> {
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 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 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 (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 pub async fn analyze_violations_async(
265 &self,
266 violations: Vec<Violation>,
267 ) -> Result<AIAnalysisReport> {
268 self.analyze_violations(violations)
269 }
270
271 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 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 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}