Skip to main content

cargo_mate/captain/
captain_log.rs

1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc};
3use colored::*;
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, VecDeque, HashSet};
6use std::fs;
7use std::path::PathBuf;
8use std::time::Duration;
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ParsedError {
11    pub code: String,
12    pub message: String,
13    pub file: String,
14    pub line: u32,
15    pub column: u32,
16    pub severity: String,
17}
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct CargoMessage {
20    pub reason: String,
21    pub message: Option<CargoDiagnostic>,
22}
23#[derive(Debug, Serialize, Deserialize, Clone)]
24pub struct CargoDiagnostic {
25    pub message: String,
26    pub code: Option<CargoErrorCode>,
27    pub level: String,
28    pub spans: Vec<CargoSpan>,
29    pub children: Vec<CargoDiagnostic>,
30    pub rendered: Option<String>,
31}
32#[derive(Debug, Serialize, Deserialize, Clone)]
33pub struct CargoErrorCode {
34    pub code: String,
35}
36#[derive(Debug, Serialize, Deserialize, Clone)]
37pub struct CargoSpan {
38    pub file_name: String,
39    pub line_start: u32,
40    pub line_end: u32,
41    pub column_start: u32,
42    pub column_end: u32,
43    pub text: Vec<CargoText>,
44    pub suggested_replacement: Option<String>,
45}
46#[derive(Debug, Serialize, Deserialize, Clone)]
47pub struct CargoText {
48    pub text: String,
49}
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct PatternCache {
52    recent_sessions: VecDeque<SessionData>,
53    max_sessions: usize,
54    error_lifecycles: HashMap<String, ErrorLifecycle>,
55    fix_patterns: HashMap<String, Vec<FixPattern>>,
56}
57#[derive(Debug, Serialize, Deserialize, Clone)]
58pub struct SessionData {
59    pub id: String,
60    pub timestamp: DateTime<Utc>,
61    pub command: String,
62    pub errors: Vec<ParsedError>,
63    pub warnings: Vec<ParsedError>,
64    pub success: bool,
65    pub duration: Duration,
66    pub files_changed: Vec<String>,
67}
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct ErrorLifecycle {
70    fingerprint: String,
71    first_seen: DateTime<Utc>,
72    last_seen: DateTime<Utc>,
73    appearances: Vec<SessionAppearance>,
74    resolution: Option<Resolution>,
75}
76#[derive(Debug, Serialize, Deserialize, Clone)]
77pub struct SessionAppearance {
78    session_id: String,
79    timestamp: DateTime<Utc>,
80    count: usize,
81}
82#[derive(Debug, Serialize, Deserialize, Clone)]
83pub struct Resolution {
84    session_id: String,
85    commands_between: Vec<String>,
86    files_changed: Vec<String>,
87    time_to_fix: Duration,
88    stayed_fixed: bool,
89}
90#[derive(Debug, Serialize, Deserialize, Clone)]
91pub struct FixPattern {
92    suggestion: String,
93    success_count: usize,
94    failure_count: usize,
95    last_used: DateTime<Utc>,
96}
97#[derive(Debug, Clone)]
98pub struct BuildImpact {
99    pub likely_errors: Vec<String>,
100    pub estimated_duration: f64,
101    pub affected_files: Vec<String>,
102}
103impl Default for BuildImpact {
104    fn default() -> BuildImpact {
105        BuildImpact {
106            likely_errors: Vec::new(),
107            estimated_duration: 0.0,
108            affected_files: Vec::new(),
109        }
110    }
111}
112impl PatternCache {
113    pub fn new() -> Result<Self> {
114        let cache_file = dirs::home_dir()
115            .unwrap()
116            .join(".shipwreck")
117            .join("pattern_cache.json");
118        if cache_file.exists() {
119            let content = fs::read_to_string(&cache_file)?;
120            Ok(serde_json::from_str(&content).unwrap_or_default())
121        } else {
122            Ok(Self {
123                recent_sessions: VecDeque::with_capacity(50),
124                max_sessions: 50,
125                error_lifecycles: HashMap::new(),
126                fix_patterns: HashMap::new(),
127            })
128        }
129    }
130    pub fn learn_from_session(&mut self, session: SessionData) -> Result<()> {
131        let prev_session = self.recent_sessions.back().cloned();
132        if let Some(prev_session) = prev_session {
133            self.detect_resolutions(&prev_session, &session);
134        }
135        for error in &session.errors {
136            let fingerprint = self.fingerprint(error);
137            self.error_lifecycles
138                .entry(fingerprint.clone())
139                .or_insert_with(|| ErrorLifecycle::new(fingerprint))
140                .add_appearance(&session.id);
141        }
142        self.recent_sessions.push_back(session);
143        while self.recent_sessions.len() > self.max_sessions {
144            self.recent_sessions.pop_front();
145        }
146        self.save()?;
147        Ok(())
148    }
149    pub fn suggest_fix(&self, error: &ParsedError) -> Option<String> {
150        let fingerprint = self.fingerprint(error);
151        if let Some(patterns) = self.fix_patterns.get(&fingerprint) {
152            patterns.iter().max_by_key(|p| p.success_count).map(|p| p.suggestion.clone())
153        } else {
154            self.find_similar_fix(&fingerprint)
155        }
156    }
157    pub fn predict_build_impact(&self, files_changed: &[String]) -> BuildImpact {
158        let mut impact = BuildImpact::default();
159        for file in files_changed {
160            for session in &self.recent_sessions {
161                if session.files_changed.contains(file) {
162                    impact.add_historical_data(&session);
163                }
164            }
165        }
166        impact
167    }
168    pub fn calculate_project_health(&self) -> ProjectHealth {
169        let total_sessions = self.recent_sessions.len();
170        let successful_sessions = self
171            .recent_sessions
172            .iter()
173            .filter(|s| s.success)
174            .count();
175        let success_rate = if total_sessions > 0 {
176            (successful_sessions as f64 / total_sessions as f64) * 100.0
177        } else {
178            0.0
179        };
180        let success_rate_trend = if total_sessions >= 10 {
181            let recent = &self.recent_sessions.iter().rev().take(5).collect::<Vec<_>>();
182            let older = &self
183                .recent_sessions
184                .iter()
185                .rev()
186                .skip(5)
187                .take(5)
188                .collect::<Vec<_>>();
189            let recent_rate = recent.iter().filter(|s| s.success).count() as f64
190                / recent.len() as f64;
191            let older_rate = older.iter().filter(|s| s.success).count() as f64
192                / older.len() as f64;
193            recent_rate - older_rate
194        } else {
195            0.0
196        };
197        let errors_per_day = if total_sessions > 0 {
198            let total_errors: usize = self
199                .recent_sessions
200                .iter()
201                .map(|s| s.errors.len())
202                .sum();
203            let days = self.recent_sessions.len() as f64 / 24.0;
204            total_errors as f64 / days.max(1.0)
205        } else {
206            0.0
207        };
208        let avg_errors_per_day = if self.recent_sessions.len() > 10 {
209            let mid_point = self.recent_sessions.len() / 2;
210            let first_half = &self
211                .recent_sessions
212                .iter()
213                .take(mid_point)
214                .collect::<Vec<_>>();
215            let second_half = &self
216                .recent_sessions
217                .iter()
218                .skip(mid_point)
219                .collect::<Vec<_>>();
220            let first_avg = first_half.iter().map(|s| s.errors.len()).sum::<usize>()
221                as f64 / first_half.len() as f64;
222            let second_avg = second_half.iter().map(|s| s.errors.len()).sum::<usize>()
223                as f64 / second_half.len() as f64;
224            (first_avg + second_avg) / 2.0
225        } else {
226            errors_per_day
227        };
228        let avg_time_to_fix = if !self.error_lifecycles.is_empty() {
229            let total_fix_time: Duration = self
230                .error_lifecycles
231                .values()
232                .filter_map(|lc| lc.resolution.as_ref())
233                .map(|r| r.time_to_fix)
234                .sum();
235            total_fix_time / self.error_lifecycles.len() as u32
236        } else {
237            Duration::ZERO
238        };
239        let top_error_hotspot = self.find_most_problematic_file();
240        ProjectHealth {
241            current_success_rate: success_rate,
242            success_rate_trend,
243            errors_per_day,
244            avg_errors_per_day,
245            avg_time_to_fix,
246            top_error_hotspot,
247        }
248    }
249    fn detect_resolutions(
250        &mut self,
251        prev_session: &SessionData,
252        current_session: &SessionData,
253    ) {
254        if prev_session.success || current_session.errors.is_empty() {
255            return;
256        }
257        let current_fingerprints: HashSet<String> = current_session
258            .errors
259            .iter()
260            .map(|e| self.fingerprint(e))
261            .collect();
262        for error in &prev_session.errors {
263            let fingerprint = self.fingerprint(error);
264            if let Some(lifecycle) = self.error_lifecycles.get_mut(&fingerprint) {
265                if !current_fingerprints.contains(&fingerprint) {
266                    let resolution = Resolution {
267                        session_id: current_session.id.clone(),
268                        commands_between: vec![],
269                        files_changed: current_session.files_changed.clone(),
270                        time_to_fix: current_session
271                            .timestamp
272                            .signed_duration_since(prev_session.timestamp)
273                            .to_std()
274                            .unwrap_or(Duration::ZERO),
275                        stayed_fixed: true,
276                    };
277                    lifecycle.resolution = Some(resolution);
278                }
279            }
280        }
281    }
282    fn fingerprint(&self, error: &ParsedError) -> String {
283        format!(
284            "{}:{}", error.code, error.message.chars().take(50).collect::< String > ()
285        )
286    }
287    fn find_similar_fix(&self, fingerprint: &str) -> Option<String> {
288        for (pattern_fp, patterns) in &self.fix_patterns {
289            if pattern_fp.contains(&fingerprint[0..10.min(fingerprint.len())]) {
290                return patterns
291                    .iter()
292                    .max_by_key(|p| p.success_count)
293                    .map(|p| p.suggestion.clone());
294            }
295        }
296        None
297    }
298    fn find_most_problematic_file(&self) -> Option<ErrorHotspot> {
299        let mut file_errors = HashMap::new();
300        for session in &self.recent_sessions {
301            for error in &session.errors {
302                if !error.file.is_empty() {
303                    *file_errors.entry(error.file.clone()).or_insert(0) += 1;
304                }
305            }
306        }
307        file_errors
308            .into_iter()
309            .max_by_key(|(_, count)| *count)
310            .map(|(file, error_count)| ErrorHotspot { file, error_count })
311    }
312    fn save(&self) -> Result<()> {
313        let cache_file = dirs::home_dir()
314            .unwrap()
315            .join(".shipwreck")
316            .join("pattern_cache.json");
317        fs::create_dir_all(cache_file.parent().unwrap())?;
318        let json = serde_json::to_string_pretty(self)?;
319        fs::write(cache_file, json)?;
320        Ok(())
321    }
322}
323impl Default for PatternCache {
324    fn default() -> Self {
325        Self {
326            recent_sessions: VecDeque::with_capacity(50),
327            max_sessions: 50,
328            error_lifecycles: HashMap::new(),
329            fix_patterns: HashMap::new(),
330        }
331    }
332}
333impl ErrorLifecycle {
334    fn new(fingerprint: String) -> Self {
335        Self {
336            fingerprint,
337            first_seen: Utc::now(),
338            last_seen: Utc::now(),
339            appearances: Vec::new(),
340            resolution: None,
341        }
342    }
343    fn add_appearance(&mut self, session_id: &str) {
344        self.appearances
345            .push(SessionAppearance {
346                session_id: session_id.to_string(),
347                timestamp: Utc::now(),
348                count: 1,
349            });
350        self.last_seen = Utc::now();
351    }
352}
353impl BuildImpact {
354    fn add_historical_data(&mut self, session: &SessionData) {
355        for error in &session.errors {
356            self.likely_errors.push(error.message.clone());
357        }
358        self.estimated_duration += session.duration.as_secs_f64();
359        self.affected_files.extend(session.files_changed.clone());
360    }
361}
362#[derive(Debug, Clone)]
363pub struct ProjectHealth {
364    pub current_success_rate: f64,
365    pub success_rate_trend: f64,
366    pub errors_per_day: f64,
367    pub avg_errors_per_day: f64,
368    pub avg_time_to_fix: Duration,
369    pub top_error_hotspot: Option<ErrorHotspot>,
370}
371#[derive(Debug, Clone)]
372pub struct ErrorHotspot {
373    pub file: String,
374    pub error_count: usize,
375}
376#[derive(Debug, Serialize, Deserialize, Clone)]
377pub struct LogEntry {
378    pub timestamp: DateTime<Utc>,
379    pub message: String,
380    pub tags: Vec<String>,
381    pub command: Option<String>,
382    pub build_result: Option<BuildResult>,
383    pub context: HashMap<String, String>,
384    pub error_code: Option<String>,
385    pub error_type: Option<String>,
386    pub file_path: Option<String>,
387    pub line_number: Option<u32>,
388    pub column_number: Option<u32>,
389    pub suggestion: Option<String>,
390    pub full_diagnostic: Option<serde_json::Value>,
391    pub resolved_in_session: Option<String>,
392    pub warning_type: Option<String>,
393    pub lint_name: Option<String>,
394    pub severity: Option<String>,
395    pub suppressed: Option<bool>,
396}
397#[derive(Debug, Serialize, Deserialize, Clone)]
398pub struct BuildResult {
399    pub success: bool,
400    pub error_count: usize,
401    pub warning_count: usize,
402    pub duration_seconds: f64,
403}
404pub struct CaptainLog {
405    entries: Vec<LogEntry>,
406    current_session: Vec<LogEntry>,
407    log_file: PathBuf,
408}
409impl CaptainLog {
410    pub fn new() -> Result<Self> {
411        let shipwreck_dir = dirs::home_dir()
412            .context("Could not find home directory")?
413            .join(".shipwreck");
414        fs::create_dir_all(&shipwreck_dir)?;
415        let log_file = shipwreck_dir.join("captain.log");
416        let entries = if log_file.exists() {
417            let content = fs::read_to_string(&log_file)?;
418            serde_json::from_str(&content).unwrap_or_default()
419        } else {
420            Vec::new()
421        };
422        Ok(Self {
423            entries,
424            current_session: Vec::new(),
425            log_file,
426        })
427    }
428    pub fn log(&mut self, message: &str, tags: Vec<String>) -> Result<()> {
429        let entry = LogEntry {
430            timestamp: Utc::now(),
431            message: message.to_string(),
432            tags,
433            command: None,
434            build_result: None,
435            context: self.capture_context(),
436            error_code: None,
437            error_type: None,
438            file_path: None,
439            line_number: None,
440            column_number: None,
441            suggestion: None,
442            full_diagnostic: None,
443            resolved_in_session: None,
444            warning_type: None,
445            lint_name: None,
446            severity: None,
447            suppressed: None,
448        };
449        self.entries.push(entry.clone());
450        self.current_session.push(entry.clone());
451        self.save()?;
452        println!("šŸ“ {}", format!("Logged: {}", message) .green());
453        if !entry.tags.is_empty() {
454            println!("   šŸ·ļø  Tags: {}", entry.tags.join(", ").dimmed());
455        }
456        Ok(())
457    }
458    pub fn log_command(&mut self, command: &str, result: BuildResult) -> Result<()> {
459        let entry = LogEntry {
460            timestamp: Utc::now(),
461            message: format!("Executed: {}", command),
462            tags: vec!["command".to_string()],
463            command: Some(command.to_string()),
464            build_result: Some(result.clone()),
465            context: self.capture_context(),
466            error_code: None,
467            error_type: None,
468            file_path: None,
469            line_number: None,
470            column_number: None,
471            suggestion: None,
472            full_diagnostic: None,
473            resolved_in_session: None,
474            warning_type: None,
475            lint_name: None,
476            severity: None,
477            suppressed: None,
478        };
479        self.entries.push(entry.clone());
480        self.current_session.push(entry);
481        self.save()?;
482        let status_icon = if result.success { "āœ…" } else { "āŒ" };
483        println!(
484            "{} Command logged: {} ({}s)", status_icon, command.cyan(), result
485            .duration_seconds
486        );
487        Ok(())
488    }
489    pub fn search(&self, query: &str) -> Vec<&LogEntry> {
490        self.entries
491            .iter()
492            .filter(|entry| {
493                entry.message.to_lowercase().contains(&query.to_lowercase())
494                    || entry
495                        .tags
496                        .iter()
497                        .any(|tag| tag.to_lowercase().contains(&query.to_lowercase()))
498            })
499            .collect()
500    }
501    pub fn search_by_tag(&self, tag: &str) -> Vec<&LogEntry> {
502        self.entries
503            .iter()
504            .filter(|entry| { entry.tags.iter().any(|t| t == tag) })
505            .collect()
506    }
507    pub fn get_recent(&self, count: usize) -> Vec<&LogEntry> {
508        let start = if self.entries.len() > count {
509            self.entries.len() - count
510        } else {
511            0
512        };
513        self.entries[start..].iter().collect()
514    }
515    pub fn get_session_logs(&self) -> &[LogEntry] {
516        &self.current_session
517    }
518    pub fn show_timeline(&self, days: i64) -> Result<()> {
519        let cutoff = Utc::now() - chrono::Duration::days(days);
520        let filtered: Vec<&LogEntry> = self
521            .entries
522            .iter()
523            .filter(|entry| entry.timestamp > cutoff)
524            .collect();
525        if filtered.is_empty() {
526            println!("No log entries in the last {} days", days);
527            return Ok(());
528        }
529        println!(
530            "{}", format!("=== Captain's Log - Last {} Days ===", days) .blue().bold()
531        );
532        let mut current_date = None;
533        for entry in filtered {
534            let entry_date = entry.timestamp.date_naive();
535            if current_date != Some(entry_date) {
536                println!(
537                    "\nšŸ“… {}", entry_date.format("%A, %B %d, %Y").to_string().yellow()
538                );
539                current_date = Some(entry_date);
540            }
541            let time = entry.timestamp.format("%H:%M:%S");
542            let icon = if entry.command.is_some() { "āš™ļø" } else { "šŸ“" };
543            print!("  {} {} - ", icon, time.to_string().dimmed());
544            if let Some(ref result) = entry.build_result {
545                let status = if result.success { "āœ…" } else { "āŒ" };
546                print!("{} ", status);
547            }
548            println!("{}", entry.message);
549            if !entry.tags.is_empty() && entry.tags != vec!["command"] {
550                println!("      šŸ·ļø  {}", entry.tags.join(", ").dimmed());
551            }
552        }
553        Ok(())
554    }
555    pub fn export(&self, path: &PathBuf, format: ExportFormat) -> Result<()> {
556        match format {
557            ExportFormat::Json => {
558                let json = serde_json::to_string_pretty(&self.entries)?;
559                fs::write(path, json)?;
560            }
561            ExportFormat::Markdown => {
562                let mut content = String::new();
563                content.push_str("# Captain's Log\n\n");
564                for entry in &self.entries {
565                    content
566                        .push_str(
567                            &format!(
568                                "## {}\n", entry.timestamp.format("%Y-%m-%d %H:%M:%S")
569                            ),
570                        );
571                    content.push_str(&format!("\n{}\n", entry.message));
572                    if !entry.tags.is_empty() {
573                        content
574                            .push_str(
575                                &format!("\n**Tags:** {}\n", entry.tags.join(", ")),
576                            );
577                    }
578                    if let Some(ref cmd) = entry.command {
579                        content.push_str(&format!("\n**Command:** `{}`\n", cmd));
580                    }
581                    if let Some(ref result) = entry.build_result {
582                        content
583                            .push_str(
584                                &format!(
585                                    "\n**Result:** {} ({} errors, {} warnings, {:.2}s)\n", if
586                                    result.success { "āœ… Success" } else { "āŒ Failed" },
587                                    result.error_count, result.warning_count, result
588                                    .duration_seconds
589                                ),
590                            );
591                    }
592                    content.push_str("\n---\n\n");
593                }
594                fs::write(path, content)?;
595            }
596            ExportFormat::Html => {
597                let mut content = String::new();
598                content
599                    .push_str(
600                        r#"<!DOCTYPE html>
601<html>
602<head>
603    <title>Captain's Log</title>
604    <style>
605        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
606               max-width: 900px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
607        .entry { background: white; padding: 15px; margin: 10px 0; border-radius: 8px; 
608                 box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
609        .timestamp { color: #666; font-size: 0.9em; }
610        .message { margin: 10px 0; font-size: 1.1em; }
611        .tags { display: inline-block; background: #e0e0e0; padding: 3px 8px; 
612                border-radius: 3px; margin: 2px; font-size: 0.85em; }
613        .success { color: green; }
614        .failure { color: red; }
615        .command { font-family: monospace; background: #f0f0f0; padding: 5px; 
616                   border-radius: 3px; }
617    </style>
618</head>
619<body>
620    <h1>āš“ Captain's Log</h1>
621"#,
622                    );
623                for entry in &self.entries {
624                    content.push_str("<div class='entry'>");
625                    content
626                        .push_str(
627                            &format!(
628                                "<div class='timestamp'>{}</div>", entry.timestamp
629                                .format("%Y-%m-%d %H:%M:%S")
630                            ),
631                        );
632                    content
633                        .push_str(
634                            &format!("<div class='message'>{}</div>", entry.message),
635                        );
636                    if !entry.tags.is_empty() {
637                        content.push_str("<div>");
638                        for tag in &entry.tags {
639                            content
640                                .push_str(&format!("<span class='tags'>{}</span>", tag));
641                        }
642                        content.push_str("</div>");
643                    }
644                    if let Some(ref cmd) = entry.command {
645                        content.push_str(&format!("<div class='command'>{}</div>", cmd));
646                    }
647                    if let Some(ref result) = entry.build_result {
648                        let class = if result.success { "success" } else { "failure" };
649                        content
650                            .push_str(
651                                &format!(
652                                    "<div class='{}'>{} - {} errors, {} warnings ({:.2}s)</div>",
653                                    class, if result.success { "āœ… Success" } else {
654                                    "āŒ Failed" }, result.error_count, result.warning_count,
655                                    result.duration_seconds
656                                ),
657                            );
658                    }
659                    content.push_str("</div>");
660                }
661                content.push_str("</body></html>");
662                fs::write(path, content)?;
663            }
664        }
665        println!("āœ… Log exported to {}", path.display());
666        Ok(())
667    }
668    pub fn analyze(&self) -> LogAnalysis {
669        let total_entries = self.entries.len();
670        let commands: Vec<&LogEntry> = self
671            .entries
672            .iter()
673            .filter(|e| e.command.is_some())
674            .collect();
675        let total_commands = commands.len();
676        let successful_builds = commands
677            .iter()
678            .filter(|e| e.build_result.as_ref().map_or(false, |r| r.success))
679            .count();
680        let failed_builds = total_commands - successful_builds;
681        let avg_build_time = if !commands.is_empty() {
682            let total_time: f64 = commands
683                .iter()
684                .filter_map(|e| e.build_result.as_ref())
685                .map(|r| r.duration_seconds)
686                .sum();
687            total_time / commands.len() as f64
688        } else {
689            0.0
690        };
691        let mut tag_frequency = HashMap::new();
692        for entry in &self.entries {
693            for tag in &entry.tags {
694                *tag_frequency.entry(tag.clone()).or_insert(0) += 1;
695            }
696        }
697        let mut most_common_tags: Vec<(String, usize)> = tag_frequency
698            .into_iter()
699            .collect();
700        most_common_tags.sort_by(|a, b| b.1.cmp(&a.1));
701        most_common_tags.truncate(5);
702        LogAnalysis {
703            total_entries,
704            total_commands,
705            successful_builds,
706            failed_builds,
707            success_rate: if total_commands > 0 {
708                (successful_builds as f64 / total_commands as f64) * 100.0
709            } else {
710                0.0
711            },
712            avg_build_time,
713            most_common_tags,
714        }
715    }
716    fn capture_context(&self) -> HashMap<String, String> {
717        let mut context = HashMap::new();
718        if let Ok(dir) = std::env::current_dir() {
719            context.insert("working_dir".to_string(), dir.to_string_lossy().to_string());
720        }
721        if let Ok(branch) = get_git_branch() {
722            context.insert("git_branch".to_string(), branch);
723        }
724        context
725    }
726    fn save(&self) -> Result<()> {
727        let json = serde_json::to_string_pretty(&self.entries)?;
728        fs::write(&self.log_file, json)?;
729        Ok(())
730    }
731}
732#[derive(Debug)]
733pub struct LogAnalysis {
734    pub total_entries: usize,
735    pub total_commands: usize,
736    pub successful_builds: usize,
737    pub failed_builds: usize,
738    pub success_rate: f64,
739    pub avg_build_time: f64,
740    pub most_common_tags: Vec<(String, usize)>,
741}
742impl LogAnalysis {
743    pub fn display(&self) {
744        println!("{}", "=== Captain's Log Analysis ===".blue().bold());
745        println!("šŸ“Š Total entries: {}", self.total_entries);
746        println!("āš™ļø  Total commands: {}", self.total_commands);
747        println!(
748            "āœ… Successful builds: {}", self.successful_builds.to_string().green()
749        );
750        println!("āŒ Failed builds: {}", self.failed_builds.to_string().red());
751        println!("šŸ“ˆ Success rate: {:.1}%", self.success_rate);
752        println!("ā±ļø  Average build time: {:.2}s", self.avg_build_time);
753        if !self.most_common_tags.is_empty() {
754            println!("\nšŸ·ļø  Most common tags:");
755            for (tag, count) in &self.most_common_tags {
756                println!("   {} ({})", tag.cyan(), count);
757            }
758        }
759    }
760}
761#[derive(Debug)]
762pub enum ExportFormat {
763    Json,
764    Markdown,
765    Html,
766}
767fn get_git_branch() -> Result<String> {
768    use std::process::Command;
769    let output = Command::new("git").args(&["branch", "--show-current"]).output();
770    match output {
771        Ok(output) if output.status.success() => {
772            let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
773            Ok(branch)
774        }
775        _ => Ok("unknown".to_string()),
776    }
777}
778pub struct CargoOutputParser;
779impl CargoOutputParser {
780    pub fn new() -> Self {
781        Self
782    }
783    pub fn parse_message(&self, json_msg: &str) -> Result<Option<CargoMessage>> {
784        match serde_json::from_str::<CargoMessage>(json_msg) {
785            Ok(msg) => Ok(Some(msg)),
786            Err(_) => Ok(None),
787        }
788    }
789    pub fn extract_error_code(&self, diagnostic: &CargoDiagnostic) -> Option<String> {
790        diagnostic.code.as_ref().map(|code| code.code.clone())
791    }
792    pub fn categorize_error(&self, diagnostic: &CargoDiagnostic) -> Option<String> {
793        if let Some(code) = &diagnostic.code {
794            if code.code.starts_with("E0") {
795                Some("type_system".to_string())
796            } else if code.code.starts_with("E03") {
797                Some("borrow_checker".to_string())
798            } else if code.code.starts_with("E04") {
799                Some("lifetime".to_string())
800            } else if code.code.starts_with("E05") {
801                Some("pattern_matching".to_string())
802            } else {
803                Some("other".to_string())
804            }
805        } else {
806            None
807        }
808    }
809    pub fn extract_suggestions(&self, diagnostic: &CargoDiagnostic) -> Vec<String> {
810        let mut suggestions = Vec::new();
811        for span in &diagnostic.spans {
812            if let Some(replacement) = &span.suggested_replacement {
813                suggestions.push(replacement.clone());
814            }
815        }
816        for child in &diagnostic.children {
817            suggestions.extend(self.extract_suggestions(child));
818        }
819        suggestions
820    }
821    pub fn create_log_entry_from_diagnostic(
822        &self,
823        diagnostic: &CargoDiagnostic,
824        session_id: &str,
825    ) -> LogEntry {
826        let error_code = self.extract_error_code(diagnostic);
827        let error_type = self.categorize_error(diagnostic);
828        let suggestions = self.extract_suggestions(diagnostic);
829        let (file_path, line_number, column_number) = if !diagnostic.spans.is_empty() {
830            let span = &diagnostic.spans[0];
831            (
832                Some(span.file_name.clone()),
833                Some(span.line_start),
834                Some(span.column_start),
835            )
836        } else {
837            (None, None, None)
838        };
839        LogEntry {
840            timestamp: Utc::now(),
841            message: diagnostic.message.clone(),
842            tags: vec![
843                diagnostic.level.clone(), error_code.clone().unwrap_or_else(|| "unknown"
844                .to_string()),
845            ],
846            command: Some(session_id.to_string()),
847            build_result: None,
848            context: HashMap::new(),
849            error_code: error_code.clone(),
850            error_type: error_type.clone(),
851            file_path,
852            line_number,
853            column_number,
854            suggestion: suggestions.first().cloned(),
855            full_diagnostic: Some(serde_json::to_value(diagnostic).unwrap_or_default()),
856            resolved_in_session: None,
857            warning_type: if diagnostic.level == "warning" { error_type } else { None },
858            lint_name: if diagnostic.level == "warning" { error_code } else { None },
859            severity: Some(diagnostic.level.clone()),
860            suppressed: Some(false),
861        }
862    }
863}
864pub struct PatternDetector {
865    entries: Vec<LogEntry>,
866}
867impl PatternDetector {
868    pub fn new(entries: Vec<LogEntry>) -> Self {
869        Self { entries }
870    }
871    pub fn find_recurring_errors(&self) -> Vec<(String, usize, Vec<String>)> {
872        let mut error_counts = HashMap::new();
873        for entry in &self.entries {
874            if let (Some(error_code), Some(file_path)) = (
875                &entry.error_code,
876                &entry.file_path,
877            ) {
878                let key = format!("{}:{}", error_code, file_path);
879                error_counts
880                    .entry(key)
881                    .or_insert_with(Vec::new)
882                    .push(entry.timestamp.to_rfc3339());
883            }
884        }
885        let mut recurring = error_counts
886            .into_iter()
887            .filter(|(_, timestamps)| timestamps.len() > 2)
888            .map(|(key, timestamps)| {
889                let parts: Vec<&str> = key.split(':').collect();
890                (key, timestamps.len(), timestamps)
891            })
892            .collect::<Vec<_>>();
893        recurring.sort_by(|a, b| b.1.cmp(&a.1));
894        recurring
895    }
896    pub fn detect_build_time_regression(&self) -> Vec<(String, f64, f64)> {
897        let mut regressions = Vec::new();
898        let mut command_times = HashMap::new();
899        for entry in &self.entries {
900            if let Some(ref result) = entry.build_result {
901                let cmd = entry
902                    .command
903                    .as_ref()
904                    .unwrap_or(&"unknown".to_string())
905                    .clone();
906                command_times
907                    .entry(cmd)
908                    .or_insert_with(Vec::new)
909                    .push(result.duration_seconds);
910            }
911        }
912        for (command, times) in command_times {
913            if times.len() >= 5 {
914                let recent_avg = times[times.len().saturating_sub(3)..]
915                    .iter()
916                    .sum::<f64>() / 3.0;
917                let older_avg = times[0..times.len().saturating_sub(3)]
918                    .iter()
919                    .sum::<f64>() / (times.len().saturating_sub(3)) as f64;
920                if recent_avg > older_avg * 1.2 {
921                    regressions.push((command, older_avg, recent_avg));
922                }
923            }
924        }
925        regressions
926    }
927    pub fn find_warning_clusters(&self) -> Vec<(String, usize)> {
928        let mut file_warnings = HashMap::new();
929        for entry in &self.entries {
930            if entry.severity.as_ref().map(|s| s == "warning").unwrap_or(false) {
931                if let Some(ref file_path) = entry.file_path {
932                    *file_warnings.entry(file_path.clone()).or_insert(0) += 1;
933                }
934            }
935        }
936        let mut clusters = file_warnings.into_iter().collect::<Vec<_>>();
937        clusters.sort_by(|a, b| b.1.cmp(&a.1));
938        clusters
939    }
940}
941pub fn check_quartermaster_status(command: &str) -> Result<bool> {
942    let license_manager = crate::license::LicenseManager::new()?;
943    match license_manager.enforce_license(command) {
944        Ok(_) => {
945            println!(
946                "āœ… Quartermaster reports: Command '{}' fully provisioned!", command
947                .green()
948            );
949            println!("   šŸ“‹ All supplies accounted for - ready to execute!");
950            Ok(true)
951        }
952        Err(e) => {
953            if e.to_string().contains("limit") {
954                println!("āš ļø  Quartermaster warning: Supply quota exceeded!");
955                println!("   šŸ“‹ Requisition more at: https://cargo.do/checkout");
956                println!("   šŸ“‹ Upgrade for unlimited command provisions");
957            } else if e.to_string().contains("License not found") {
958                println!("āŒ Quartermaster emergency: No provision authorization!");
959                println!("   šŸ“‹ Get requisition with 'cm register <key>'");
960            } else {
961                println!(
962                    "āŒ Quartermaster distress: Status check failed: {}", e.to_string()
963                    .red()
964                );
965                println!("   šŸ“‹ Secure the manifest - prepare for audit!");
966            }
967            Ok(false)
968        }
969    }
970}
971pub fn show_build_health_dashboard() -> Result<()> {
972    let pattern_cache = PatternCache::new().unwrap_or_default();
973    let health = pattern_cache.calculate_project_health();
974    println!("\nšŸ“Š Project Health Dashboard:");
975    let trend = if health.success_rate_trend > 0.0 { "šŸ“ˆ" } else { "šŸ“‰" };
976    println!(
977        "  {} Success Rate: {:.1}% {}", trend, health.current_success_rate,
978        format!("({:+.1}%)", health.success_rate_trend) .dimmed()
979    );
980    let error_trend = if health.errors_per_day < health.avg_errors_per_day {
981        "šŸ“‰"
982    } else {
983        "šŸ“ˆ"
984    };
985    println!(
986        "  {} Error Rate: {:.1}/day {}", error_trend, health.errors_per_day, if health
987        .errors_per_day < health.avg_errors_per_day { "(improving)" } else {
988        "(worsening)" }
989    );
990    println!("  ā±ļø  Avg Fix Time: {}", format_duration(health.avg_time_to_fix));
991    if let Some(hotspot) = health.top_error_hotspot {
992        println!("  šŸ”„ Hotspot: {} ({} errors)", hotspot.file, hotspot.error_count);
993    }
994    if !pattern_cache.recent_sessions.is_empty() {
995        println!("\nšŸŽÆ Recent Insights:");
996        let recent_sessions = pattern_cache.recent_sessions.iter().rev().take(3);
997        for session in recent_sessions {
998            let status = if session.success { "āœ…" } else { "āŒ" };
999            println!(
1000                "  {} {} ({} errors, {:.1}s)", status, session.command, session.errors
1001                .len(), session.duration.as_secs_f64()
1002            );
1003        }
1004    }
1005    Ok(())
1006}
1007fn format_duration(duration: Duration) -> String {
1008    let seconds = duration.as_secs_f32() as u64;
1009    if seconds < 60 {
1010        format!("{}s", seconds)
1011    } else if seconds < 3600 {
1012        format!("{}m {}s", seconds / 60, seconds % 60)
1013    } else {
1014        format!("{}h {}m", seconds / 3600, (seconds % 3600) / 60)
1015    }
1016}
1017pub fn detect_changed_files() -> Vec<String> {
1018    use std::process::Command;
1019    let output = Command::new("git").args(&["diff", "--name-only", "HEAD"]).output();
1020    match output {
1021        Ok(output) if output.status.success() => {
1022            String::from_utf8_lossy(&output.stdout)
1023                .lines()
1024                .map(|s| s.to_string())
1025                .collect()
1026        }
1027        _ => vec![],
1028    }
1029}
1030pub fn generate_session_id() -> String {
1031    format!("session_{}", rand::random::< u64 > ())
1032}