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}