duende_policy/
gate.rs

1//! Quality gate enforcement.
2//!
3//! # Toyota Way: Jidoka (自働化)
4//! Automatic stop when quality problems detected.
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::Result;
9
10/// Quality gate configuration.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GateConfig {
13    /// Maximum cyclomatic complexity.
14    pub max_complexity: u32,
15    /// SATD (Self-Admitted Technical Debt) tolerance.
16    pub satd_tolerance: u32,
17    /// Maximum dead code percentage.
18    pub dead_code_max_percent: f64,
19    /// Minimum quality score (0-100).
20    pub min_quality_score: f64,
21}
22
23impl Default for GateConfig {
24    fn default() -> Self {
25        Self {
26            max_complexity: 20,
27            satd_tolerance: 0,
28            dead_code_max_percent: 10.0,
29            min_quality_score: 80.0,
30        }
31    }
32}
33
34/// Quality gate for daemon code analysis.
35pub struct QualityGate {
36    config: GateConfig,
37}
38
39impl QualityGate {
40    /// Creates a new quality gate with given configuration.
41    #[must_use]
42    pub const fn new(config: GateConfig) -> Self {
43        Self { config }
44    }
45
46    /// Analyzes code quality and returns result.
47    ///
48    /// # Errors
49    /// Returns an error if analysis fails.
50    pub fn analyze(&self, analysis: &QualityAnalysis) -> Result<GateResult> {
51        let mut violations = Vec::new();
52
53        // Check complexity
54        if analysis.max_complexity > self.config.max_complexity {
55            violations.push(QualityViolation::Complexity {
56                actual: analysis.max_complexity,
57                threshold: self.config.max_complexity,
58                location: analysis.complexity_hotspot.clone(),
59            });
60        }
61
62        // Check SATD
63        if analysis.satd_count > self.config.satd_tolerance {
64            violations.push(QualityViolation::TechnicalDebt {
65                count: analysis.satd_count,
66                tolerance: self.config.satd_tolerance,
67            });
68        }
69
70        // Check dead code
71        if analysis.dead_code_percent > self.config.dead_code_max_percent {
72            violations.push(QualityViolation::DeadCode {
73                percent: analysis.dead_code_percent,
74                threshold: self.config.dead_code_max_percent,
75            });
76        }
77
78        // Check quality score
79        if analysis.quality_score < self.config.min_quality_score {
80            violations.push(QualityViolation::QualityScore {
81                score: analysis.quality_score,
82                minimum: self.config.min_quality_score,
83            });
84        }
85
86        if violations.is_empty() {
87            Ok(GateResult::Passed)
88        } else {
89            Ok(GateResult::Failed { violations })
90        }
91    }
92
93    /// Returns the gate configuration.
94    #[must_use]
95    pub const fn config(&self) -> &GateConfig {
96        &self.config
97    }
98}
99
100impl Default for QualityGate {
101    fn default() -> Self {
102        Self::new(GateConfig::default())
103    }
104}
105
106/// Result of quality gate check.
107#[derive(Debug, Clone)]
108pub enum GateResult {
109    /// Gate passed.
110    Passed,
111    /// Gate failed with violations.
112    Failed {
113        /// List of violations.
114        violations: Vec<QualityViolation>,
115    },
116}
117
118impl GateResult {
119    /// Returns true if the gate passed.
120    #[must_use]
121    pub const fn passed(&self) -> bool {
122        matches!(self, Self::Passed)
123    }
124}
125
126/// Quality violation.
127#[derive(Debug, Clone)]
128pub enum QualityViolation {
129    /// Complexity threshold exceeded.
130    Complexity {
131        /// Actual complexity.
132        actual: u32,
133        /// Threshold.
134        threshold: u32,
135        /// Location of hotspot.
136        location: Option<String>,
137    },
138    /// Technical debt tolerance exceeded.
139    TechnicalDebt {
140        /// SATD count.
141        count: u32,
142        /// Tolerance.
143        tolerance: u32,
144    },
145    /// Dead code threshold exceeded.
146    DeadCode {
147        /// Actual percentage.
148        percent: f64,
149        /// Threshold.
150        threshold: f64,
151    },
152    /// Quality score below minimum.
153    QualityScore {
154        /// Actual score.
155        score: f64,
156        /// Minimum required.
157        minimum: f64,
158    },
159}
160
161/// Quality analysis input.
162#[derive(Debug, Clone, Default)]
163pub struct QualityAnalysis {
164    /// Maximum cyclomatic complexity found.
165    pub max_complexity: u32,
166    /// Location of complexity hotspot.
167    pub complexity_hotspot: Option<String>,
168    /// SATD comment count.
169    pub satd_count: u32,
170    /// Dead code percentage.
171    pub dead_code_percent: f64,
172    /// Overall quality score (0-100).
173    pub quality_score: f64,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_gate_passes_good_code() {
182        let gate = QualityGate::default();
183        let analysis = QualityAnalysis {
184            max_complexity: 10,
185            satd_count: 0,
186            dead_code_percent: 5.0,
187            quality_score: 90.0,
188            ..Default::default()
189        };
190
191        let result = gate.analyze(&analysis).ok();
192        assert!(result.is_some_and(|r| r.passed()));
193    }
194
195    #[test]
196    fn test_gate_fails_on_complexity() {
197        let gate = QualityGate::new(GateConfig {
198            max_complexity: 10,
199            ..Default::default()
200        });
201
202        let analysis = QualityAnalysis {
203            max_complexity: 25,
204            ..Default::default()
205        };
206
207        let result = gate.analyze(&analysis).ok();
208        assert!(result.is_some_and(|r| !r.passed()));
209    }
210
211    #[test]
212    fn test_gate_fails_on_satd() {
213        let gate = QualityGate::new(GateConfig {
214            satd_tolerance: 0,
215            ..Default::default()
216        });
217
218        let analysis = QualityAnalysis {
219            satd_count: 5,
220            quality_score: 90.0,
221            ..Default::default()
222        };
223
224        let result = gate.analyze(&analysis).ok();
225        assert!(result.is_some_and(|r| !r.passed()));
226    }
227}