Skip to main content

omni_dev/data/
check.rs

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