Skip to main content

semver_analyzer_core/
diagnostics.rs

1//! Degradation tracking for non-fatal issues during analysis.
2//!
3//! The [`DegradationTracker`] collects issues that degrade analysis quality
4//! without causing a fatal error. It is thread-safe and accessible via
5//! `SharedFindings::degradation()` so all pipeline phases and Language
6//! implementations can record issues.
7//!
8//! At the end of a run, the CLI renders a summary of all recorded issues
9//! so the user knows what parts of the analysis may be incomplete.
10//!
11//! ## When to Record a Degradation
12//!
13//! - A pipeline phase fails but execution can continue with partial results
14//! - An external tool (LLM, CSS extraction, dep repo build) fails
15//! - Multiple per-item failures occur (batch into a single summary entry)
16//!
17//! ## When NOT to Record
18//!
19//! - Best-effort operations where failure is a normal code path
20//!   (e.g., `read_git_file` returning `None` for a file that may not exist)
21//! - Cleanup/teardown failures (Drop impls, worktree removal)
22
23use std::sync::Mutex;
24
25/// Tracks non-fatal issues that degrade analysis quality.
26///
27/// Thread-safe — wrap in `Arc` and share across pipeline phases.
28/// Lives on `SharedFindings` for convenient access.
29#[derive(Debug, Default)]
30pub struct DegradationTracker {
31    issues: Mutex<Vec<DegradationIssue>>,
32}
33
34/// A single non-fatal issue recorded during analysis.
35#[derive(Debug, Clone)]
36pub struct DegradationIssue {
37    /// Short pipeline phase tag: "TD", "SD", "BU", "CSS", "LLM".
38    pub phase: String,
39    /// What happened (technical, concise).
40    pub message: String,
41    /// What the user is missing as a result (user-facing, actionable).
42    pub impact: String,
43}
44
45impl DegradationTracker {
46    /// Create a new empty tracker.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Record a non-fatal issue.
52    ///
53    /// # Arguments
54    ///
55    /// * `phase` — Short tag identifying the pipeline phase ("TD", "SD", "BU", "CSS", "LLM")
56    /// * `message` — What happened (technical, concise)
57    /// * `impact` — What the user is missing (user-facing, actionable)
58    pub fn record(
59        &self,
60        phase: impl Into<String>,
61        message: impl Into<String>,
62        impact: impl Into<String>,
63    ) {
64        self.issues.lock().unwrap().push(DegradationIssue {
65            phase: phase.into(),
66            message: message.into(),
67            impact: impact.into(),
68        });
69    }
70
71    /// Get a snapshot of all recorded issues.
72    pub fn issues(&self) -> Vec<DegradationIssue> {
73        self.issues.lock().unwrap().clone()
74    }
75
76    /// Check if any issues have been recorded.
77    pub fn has_issues(&self) -> bool {
78        !self.issues.lock().unwrap().is_empty()
79    }
80
81    /// Get the count of recorded issues.
82    pub fn issue_count(&self) -> usize {
83        self.issues.lock().unwrap().len()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::sync::Arc;
91
92    #[test]
93    fn empty_tracker_has_no_issues() {
94        let tracker = DegradationTracker::new();
95        assert!(!tracker.has_issues());
96        assert_eq!(tracker.issue_count(), 0);
97        assert!(tracker.issues().is_empty());
98    }
99
100    #[test]
101    fn record_and_retrieve_issues() {
102        let tracker = DegradationTracker::new();
103        tracker.record(
104            "SD",
105            "Source-level analysis failed",
106            "Composition trees unavailable",
107        );
108        tracker.record(
109            "CSS",
110            "CSS extraction failed",
111            "CSS removal rules incomplete",
112        );
113
114        assert!(tracker.has_issues());
115        assert_eq!(tracker.issue_count(), 2);
116
117        let issues = tracker.issues();
118        assert_eq!(issues[0].phase, "SD");
119        assert_eq!(issues[0].message, "Source-level analysis failed");
120        assert_eq!(issues[0].impact, "Composition trees unavailable");
121        assert_eq!(issues[1].phase, "CSS");
122    }
123
124    #[test]
125    fn thread_safe_recording() {
126        let tracker = Arc::new(DegradationTracker::new());
127        let mut handles = Vec::new();
128
129        for i in 0..10 {
130            let tracker = tracker.clone();
131            handles.push(std::thread::spawn(move || {
132                tracker.record("TEST", format!("Issue {}", i), "Test impact");
133            }));
134        }
135
136        for handle in handles {
137            handle.join().unwrap();
138        }
139
140        assert_eq!(tracker.issue_count(), 10);
141    }
142}