Skip to main content

lexicon_spec/
gates.rs

1use serde::{Deserialize, Serialize};
2
3use crate::common::DimensionCategory;
4use crate::version::SchemaVersion;
5
6/// The gates model defines verification checks that must pass.
7///
8/// Gates are concrete commands that run during `lexicon verify`.
9/// Required gates block the build. Scored gates contribute to the
10/// overall score. Advisory gates are informational.
11///
12/// Stored at `specs/gates.toml`.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GatesModel {
15    pub schema_version: SchemaVersion,
16    #[serde(default)]
17    pub gates: Vec<Gate>,
18}
19
20/// A single verification gate.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Gate {
23    /// Unique identifier, e.g. "fmt", "clippy", "unit-tests".
24    pub id: String,
25    /// Human-readable label.
26    pub label: String,
27    /// Shell command to execute.
28    pub command: String,
29    /// Whether this gate is required, scored, or advisory.
30    #[serde(default)]
31    pub category: DimensionCategory,
32    /// Timeout in seconds. None means use default (300s).
33    #[serde(default)]
34    pub timeout_secs: Option<u64>,
35    /// Whether this gate may be skipped. Required gates default to false.
36    #[serde(default)]
37    pub allow_skip: bool,
38}
39
40impl GatesModel {
41    /// Create a default gates model with standard Rust gates.
42    pub fn default_model() -> Self {
43        Self {
44            schema_version: SchemaVersion::CURRENT,
45            gates: vec![
46                Gate {
47                    id: "fmt".to_string(),
48                    label: "Format Check".to_string(),
49                    command: "cargo fmt -- --check".to_string(),
50                    category: DimensionCategory::Required,
51                    timeout_secs: Some(60),
52                    allow_skip: false,
53                },
54                Gate {
55                    id: "clippy".to_string(),
56                    label: "Clippy Lints".to_string(),
57                    command: "cargo clippy -- -D warnings".to_string(),
58                    category: DimensionCategory::Required,
59                    timeout_secs: Some(120),
60                    allow_skip: false,
61                },
62                Gate {
63                    id: "unit-tests".to_string(),
64                    label: "Unit Tests".to_string(),
65                    command: "cargo test".to_string(),
66                    category: DimensionCategory::Required,
67                    timeout_secs: Some(300),
68                    allow_skip: false,
69                },
70                Gate {
71                    id: "doc-tests".to_string(),
72                    label: "Documentation Tests".to_string(),
73                    command: "cargo test --doc".to_string(),
74                    category: DimensionCategory::Scored,
75                    timeout_secs: Some(120),
76                    allow_skip: true,
77                },
78            ],
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_gates_model_toml_roundtrip() {
89        let model = GatesModel::default_model();
90        let toml_str = toml::to_string_pretty(&model).unwrap();
91        let parsed: GatesModel = toml::from_str(&toml_str).unwrap();
92        assert_eq!(parsed.gates.len(), 4);
93        assert_eq!(parsed.gates[0].id, "fmt");
94        assert!(!parsed.gates[0].allow_skip);
95    }
96
97    #[test]
98    fn test_required_gates_cannot_skip() {
99        let model = GatesModel::default_model();
100        for gate in &model.gates {
101            if gate.category == DimensionCategory::Required {
102                assert!(!gate.allow_skip, "Required gate {} must not allow skip", gate.id);
103            }
104        }
105    }
106}