Skip to main content

ralph/commands/doctor/
types.rs

1//! Types for doctor checks and reports.
2//!
3//! Responsibilities:
4//! - Define severity levels, check results, and report structures.
5//! - Carry canonical blocking-state diagnostics alongside doctor results.
6//!
7//! Not handled here:
8//! - Actual check implementations (see submodules).
9//! - Output formatting (see output.rs).
10//!
11//! Invariants/assumptions:
12//! - CheckResult factories are pure functions with no side effects.
13//! - DoctorReport maintains consistent summary statistics.
14//! - BlockingState, when present, uses the canonical operator-facing contract.
15
16use crate::contracts::BlockingState;
17use serde::Serialize;
18
19/// Severity level for a doctor check.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "PascalCase")]
22pub enum CheckSeverity {
23    /// Check passed successfully.
24    Success,
25    /// Non-critical issue, operation can continue.
26    Warning,
27    /// Critical issue, operation should not proceed.
28    Error,
29}
30
31/// A single check result.
32#[derive(Debug, Clone, Serialize)]
33pub struct CheckResult {
34    /// Category of the check (git, queue, runner, project, lock).
35    pub category: String,
36    /// Specific check name (e.g., "git_binary", "queue_valid").
37    pub check: String,
38    /// Severity level of the result.
39    pub severity: CheckSeverity,
40    /// Human-readable message describing the result.
41    pub message: String,
42    /// Whether a fix is available for this issue.
43    pub fix_available: bool,
44    /// Whether a fix was applied (None if not attempted, Some(true/false) if attempted).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub fix_applied: Option<bool>,
47    /// Suggested fix or action for the user.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub suggested_fix: Option<String>,
50    /// Canonical operator-facing blocking diagnosis for this check.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub blocking: Option<BlockingState>,
53}
54
55impl CheckResult {
56    /// Create a successful check result.
57    pub fn success(category: &str, check: &str, message: &str) -> Self {
58        Self {
59            category: category.to_string(),
60            check: check.to_string(),
61            severity: CheckSeverity::Success,
62            message: message.to_string(),
63            fix_available: false,
64            fix_applied: None,
65            suggested_fix: None,
66            blocking: None,
67        }
68    }
69
70    /// Create a warning check result.
71    pub fn warning(
72        category: &str,
73        check: &str,
74        message: &str,
75        fix_available: bool,
76        suggested_fix: Option<&str>,
77    ) -> Self {
78        Self {
79            category: category.to_string(),
80            check: check.to_string(),
81            severity: CheckSeverity::Warning,
82            message: message.to_string(),
83            fix_available,
84            fix_applied: None,
85            suggested_fix: suggested_fix.map(|s| s.to_string()),
86            blocking: None,
87        }
88    }
89
90    /// Create an error check result.
91    pub fn error(
92        category: &str,
93        check: &str,
94        message: &str,
95        fix_available: bool,
96        suggested_fix: Option<&str>,
97    ) -> Self {
98        Self {
99            category: category.to_string(),
100            check: check.to_string(),
101            severity: CheckSeverity::Error,
102            message: message.to_string(),
103            fix_available,
104            fix_applied: None,
105            suggested_fix: suggested_fix.map(|s| s.to_string()),
106            blocking: None,
107        }
108    }
109
110    /// Mark that a fix was applied to this check.
111    pub fn with_fix_applied(mut self, applied: bool) -> Self {
112        self.fix_applied = Some(applied);
113        self
114    }
115
116    /// Attach the canonical blocking-state explanation for this check.
117    pub fn with_blocking(mut self, blocking: BlockingState) -> Self {
118        self.blocking = Some(blocking);
119        self
120    }
121}
122
123/// Summary of all checks.
124#[derive(Debug, Clone, Serialize)]
125pub struct Summary {
126    /// Total number of checks performed.
127    pub total: usize,
128    /// Number of successful checks.
129    pub passed: usize,
130    /// Number of warnings.
131    pub warnings: usize,
132    /// Number of errors.
133    pub errors: usize,
134    /// Number of fixes applied.
135    pub fixes_applied: usize,
136    /// Number of fixes that failed.
137    pub fixes_failed: usize,
138}
139
140/// Full doctor report (for JSON output).
141#[derive(Debug, Clone, Serialize)]
142pub struct DoctorReport {
143    /// Overall success status (true if no errors).
144    pub success: bool,
145    /// Canonical operator-facing blocking diagnosis for the current repo state.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub blocking: Option<BlockingState>,
148    /// Individual check results.
149    pub checks: Vec<CheckResult>,
150    /// Summary statistics.
151    pub summary: Summary,
152}
153
154impl DoctorReport {
155    /// Create a new empty report.
156    pub fn new() -> Self {
157        Self {
158            success: true,
159            blocking: None,
160            checks: Vec::new(),
161            summary: Summary {
162                total: 0,
163                passed: 0,
164                warnings: 0,
165                errors: 0,
166                fixes_applied: 0,
167                fixes_failed: 0,
168            },
169        }
170    }
171
172    /// Add a check result to the report.
173    pub fn add(&mut self, result: CheckResult) {
174        self.summary.total += 1;
175        match result.severity {
176            CheckSeverity::Success => self.summary.passed += 1,
177            CheckSeverity::Warning => self.summary.warnings += 1,
178            CheckSeverity::Error => {
179                self.summary.errors += 1;
180                self.success = false;
181            }
182        }
183        if result.fix_applied == Some(true) {
184            self.summary.fixes_applied += 1;
185        } else if result.fix_applied == Some(false) {
186            self.summary.fixes_failed += 1;
187        }
188        self.checks.push(result);
189    }
190}
191
192impl Default for DoctorReport {
193    fn default() -> Self {
194        Self::new()
195    }
196}