Skip to main content

dkp_core/validate/
gates.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4pub enum Gate {
5    /// Machine usability — schemas, required files, graph edge resolution
6    Gate4 = 4,
7    /// Evaluation — eval_set.jsonl present and achieves min_eval_delta
8    Gate7 = 7,
9    /// OKF conformance — frontmatter, link resolution, bundle signature
10    Gate8 = 8,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum GateStatus {
15    Pass,
16    Fail,
17    /// Gate was not run (e.g., optional asset not present)
18    Skipped,
19    /// Gate not applicable (e.g., OKF layer absent for gate 8)
20    NotApplicable,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct GateResult {
25    pub gate: u8,
26    pub status: GateStatus,
27    pub checks: Vec<CheckResult>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub message: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CheckResult {
34    pub description: String,
35    pub status: GateStatus,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub detail: Option<String>,
38}
39
40impl CheckResult {
41    pub fn pass(description: impl Into<String>) -> Self {
42        Self {
43            description: description.into(),
44            status: GateStatus::Pass,
45            detail: None,
46        }
47    }
48
49    pub fn fail(description: impl Into<String>, detail: impl Into<String>) -> Self {
50        Self {
51            description: description.into(),
52            status: GateStatus::Fail,
53            detail: Some(detail.into()),
54        }
55    }
56
57    pub fn skip(description: impl Into<String>) -> Self {
58        Self {
59            description: description.into(),
60            status: GateStatus::Skipped,
61            detail: None,
62        }
63    }
64}
65
66#[derive(Debug, Serialize, Deserialize)]
67pub struct ValidationReport {
68    pub pack_name: String,
69    pub pack_version: String,
70    pub gates: Vec<GateResult>,
71    pub overall: GateStatus,
72    pub conformance: ConformanceLevel,
73    pub reviewed: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub enum ConformanceLevel {
78    /// Gates 4 and 8 passed
79    DkpConformant,
80    /// Gates 4, 7, and 8 passed; review_notes attests human editorial review
81    DkpReviewed,
82    /// Not conformant
83    NonConformant,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn check_result_pass_sets_pass_status_and_no_detail() {
92        let c = CheckResult::pass("thing checked");
93        assert_eq!(c.status, GateStatus::Pass);
94        assert_eq!(c.description, "thing checked");
95        assert!(c.detail.is_none());
96    }
97
98    #[test]
99    fn check_result_fail_sets_fail_status_and_detail() {
100        let c = CheckResult::fail("thing checked", "why it failed");
101        assert_eq!(c.status, GateStatus::Fail);
102        assert_eq!(c.detail.as_deref(), Some("why it failed"));
103    }
104
105    #[test]
106    fn check_result_skip_sets_skipped_status_and_no_detail() {
107        let c = CheckResult::skip("thing checked");
108        assert_eq!(c.status, GateStatus::Skipped);
109        assert!(c.detail.is_none());
110    }
111
112    #[test]
113    fn gate_status_serde_round_trip() {
114        for status in [
115            GateStatus::Pass,
116            GateStatus::Fail,
117            GateStatus::Skipped,
118            GateStatus::NotApplicable,
119        ] {
120            let json = serde_json::to_string(&status).unwrap();
121            let back: GateStatus = serde_json::from_str(&json).unwrap();
122            assert_eq!(status, back);
123        }
124    }
125
126    #[test]
127    fn conformance_level_serde_round_trip() {
128        for level in [
129            ConformanceLevel::DkpConformant,
130            ConformanceLevel::DkpReviewed,
131            ConformanceLevel::NonConformant,
132        ] {
133            let json = serde_json::to_string(&level).unwrap();
134            let back: ConformanceLevel = serde_json::from_str(&json).unwrap();
135            assert_eq!(level, back);
136        }
137    }
138
139    #[test]
140    fn gate_result_serde_round_trip_omits_none_message() {
141        let result = GateResult {
142            gate: 4,
143            status: GateStatus::Pass,
144            checks: vec![CheckResult::pass("ok")],
145            message: None,
146        };
147        let json = serde_json::to_string(&result).unwrap();
148        assert!(!json.contains("\"message\""));
149        let back: GateResult = serde_json::from_str(&json).unwrap();
150        assert_eq!(back.gate, 4);
151        assert_eq!(back.status, GateStatus::Pass);
152        assert_eq!(back.checks.len(), 1);
153    }
154}