omni_dev/data/
check.rs

1//! Check command result types for commit message validation
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Complete check report containing all commit analysis results
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct CheckReport {
9    /// Individual commit check results
10    pub commits: Vec<CommitCheckResult>,
11    /// Summary statistics
12    pub summary: CheckSummary,
13}
14
15/// Result of checking a single commit
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CommitCheckResult {
18    /// Commit hash (short form)
19    pub hash: String,
20    /// Original commit message (first line)
21    pub message: String,
22    /// List of issues found
23    pub issues: Vec<CommitIssue>,
24    /// Suggested improved message (if issues were found)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub suggestion: Option<CommitSuggestion>,
27    /// Whether the commit passes all checks
28    pub passes: bool,
29}
30
31/// A single issue found in a commit message
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CommitIssue {
34    /// Severity level of the issue
35    pub severity: IssueSeverity,
36    /// Which guideline section was violated
37    pub section: String,
38    /// Specific rule that was violated
39    pub rule: String,
40    /// Explanation of why this is a violation
41    pub explanation: String,
42}
43
44/// Suggested correction for a commit message
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CommitSuggestion {
47    /// The suggested improved commit message
48    pub message: String,
49    /// Explanation of why this message is better
50    pub explanation: String,
51}
52
53/// Severity level for issues
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum IssueSeverity {
57    /// Errors block CI (exit code 1)
58    Error,
59    /// Advisory issues (exit code 0, or 2 with --strict)
60    Warning,
61    /// Suggestions only (never affect exit code)
62    Info,
63}
64
65impl fmt::Display for IssueSeverity {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            IssueSeverity::Error => write!(f, "ERROR"),
69            IssueSeverity::Warning => write!(f, "WARNING"),
70            IssueSeverity::Info => write!(f, "INFO"),
71        }
72    }
73}
74
75impl std::str::FromStr for IssueSeverity {
76    type Err = ();
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s.to_lowercase().as_str() {
80            "error" => Ok(IssueSeverity::Error),
81            "warning" => Ok(IssueSeverity::Warning),
82            "info" => Ok(IssueSeverity::Info),
83            _ => Ok(IssueSeverity::Warning), // default
84        }
85    }
86}
87
88impl IssueSeverity {
89    /// Parse severity from string (case-insensitive)
90    pub fn parse(s: &str) -> Self {
91        s.parse().unwrap_or(IssueSeverity::Warning)
92    }
93}
94
95/// Summary statistics for a check report
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CheckSummary {
98    /// Total number of commits checked
99    pub total_commits: usize,
100    /// Number of commits that pass all checks
101    pub passing_commits: usize,
102    /// Number of commits with issues
103    pub failing_commits: usize,
104    /// Total number of errors found
105    pub error_count: usize,
106    /// Total number of warnings found
107    pub warning_count: usize,
108    /// Total number of info-level issues found
109    pub info_count: usize,
110}
111
112impl CheckSummary {
113    /// Create a summary from a list of commit check results
114    pub fn from_results(results: &[CommitCheckResult]) -> Self {
115        let total_commits = results.len();
116        let passing_commits = results.iter().filter(|r| r.passes).count();
117        let failing_commits = total_commits - passing_commits;
118
119        let mut error_count = 0;
120        let mut warning_count = 0;
121        let mut info_count = 0;
122
123        for result in results {
124            for issue in &result.issues {
125                match issue.severity {
126                    IssueSeverity::Error => error_count += 1,
127                    IssueSeverity::Warning => warning_count += 1,
128                    IssueSeverity::Info => info_count += 1,
129                }
130            }
131        }
132
133        Self {
134            total_commits,
135            passing_commits,
136            failing_commits,
137            error_count,
138            warning_count,
139            info_count,
140        }
141    }
142}
143
144impl CheckReport {
145    /// Create a new check report from commit results
146    pub fn new(commits: Vec<CommitCheckResult>) -> Self {
147        let summary = CheckSummary::from_results(&commits);
148        Self { commits, summary }
149    }
150
151    /// Check if the report has any errors
152    pub fn has_errors(&self) -> bool {
153        self.summary.error_count > 0
154    }
155
156    /// Check if the report has any warnings
157    pub fn has_warnings(&self) -> bool {
158        self.summary.warning_count > 0
159    }
160
161    /// Determine exit code based on report and options
162    pub fn exit_code(&self, strict: bool) -> i32 {
163        if self.has_errors() {
164            1
165        } else if strict && self.has_warnings() {
166            2
167        } else {
168            0
169        }
170    }
171}
172
173/// Output format for check results
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
175pub enum OutputFormat {
176    /// Human-readable text format
177    #[default]
178    Text,
179    /// JSON format
180    Json,
181    /// YAML format
182    Yaml,
183}
184
185impl std::str::FromStr for OutputFormat {
186    type Err = ();
187
188    fn from_str(s: &str) -> Result<Self, Self::Err> {
189        match s.to_lowercase().as_str() {
190            "text" => Ok(OutputFormat::Text),
191            "json" => Ok(OutputFormat::Json),
192            "yaml" => Ok(OutputFormat::Yaml),
193            _ => Err(()),
194        }
195    }
196}
197
198impl fmt::Display for OutputFormat {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            OutputFormat::Text => write!(f, "text"),
202            OutputFormat::Json => write!(f, "json"),
203            OutputFormat::Yaml => write!(f, "yaml"),
204        }
205    }
206}
207
208/// AI response structure for parsing check results
209#[derive(Debug, Clone, Deserialize)]
210pub struct AiCheckResponse {
211    /// List of commit checks
212    pub checks: Vec<AiCommitCheck>,
213}
214
215/// Single commit check from AI response
216#[derive(Debug, Clone, Deserialize)]
217pub struct AiCommitCheck {
218    /// Commit hash (short or full)
219    pub commit: String,
220    /// Whether the commit passes all checks
221    pub passes: bool,
222    /// List of issues found
223    #[serde(default)]
224    pub issues: Vec<AiIssue>,
225    /// Suggested message improvement
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub suggestion: Option<AiSuggestion>,
228}
229
230/// Issue from AI response
231#[derive(Debug, Clone, Deserialize)]
232pub struct AiIssue {
233    /// Severity level
234    pub severity: String,
235    /// Guideline section
236    pub section: String,
237    /// Specific rule violated
238    pub rule: String,
239    /// Explanation
240    pub explanation: String,
241}
242
243/// Suggestion from AI response
244#[derive(Debug, Clone, Deserialize)]
245pub struct AiSuggestion {
246    /// Suggested message
247    pub message: String,
248    /// Explanation of improvements
249    pub explanation: String,
250}
251
252impl From<AiCommitCheck> for CommitCheckResult {
253    fn from(ai: AiCommitCheck) -> Self {
254        let issues: Vec<CommitIssue> = ai
255            .issues
256            .into_iter()
257            .map(|i| CommitIssue {
258                severity: IssueSeverity::parse(&i.severity),
259                section: i.section,
260                rule: i.rule,
261                explanation: i.explanation,
262            })
263            .collect();
264
265        let suggestion = ai.suggestion.map(|s| CommitSuggestion {
266            message: s.message,
267            explanation: s.explanation,
268        });
269
270        Self {
271            hash: ai.commit,
272            message: String::new(), // Will be filled in by caller
273            issues,
274            suggestion,
275            passes: ai.passes,
276        }
277    }
278}