Skip to main content

dkp_core/validate/
gate8.rs

1use crate::{
2    okf::parser::parse_okf_dir,
3    pack::loader::Pack,
4    validate::gates::{CheckResult, GateResult, GateStatus},
5};
6
7/// Gate 8: OKF Conformance — frontmatter valid, type field present, bundle.sig present.
8pub fn run(pack: &Pack) -> GateResult {
9    if !pack.has_okf() {
10        return GateResult {
11            gate: 8,
12            status: GateStatus::NotApplicable,
13            checks: vec![CheckResult::skip("okf/ layer (not present)")],
14            message: Some("OKF layer absent; gate 8 not applicable".to_string()),
15        };
16    }
17
18    let mut checks = Vec::new();
19
20    // Walk and parse all OKF concept files
21    match parse_okf_dir(&pack.okf_dir()) {
22        Ok(concepts) => {
23            let n = concepts.len();
24            checks.push(CheckResult::pass(format!("OKF concept files: {n} parsed")));
25
26            // Every concept must have a 'type' key in frontmatter
27            let missing_type: Vec<String> = concepts
28                .iter()
29                .filter(|c| c.frontmatter.get("type").is_none())
30                .map(|c| {
31                    c.path
32                        .file_name()
33                        .unwrap_or_default()
34                        .to_string_lossy()
35                        .into_owned()
36                })
37                .collect();
38
39            if missing_type.is_empty() {
40                checks.push(CheckResult::pass(format!(
41                    "OKF frontmatter: all {n} files have 'type' field"
42                )));
43            } else {
44                checks.push(CheckResult::fail(
45                    "OKF frontmatter: 'type' field required",
46                    format!("missing 'type': {}", missing_type.join(", ")),
47                ));
48            }
49        }
50        Err(e) => {
51            checks.push(CheckResult::fail("OKF concept files parse", e.to_string()));
52        }
53    }
54
55    // bundle.sig presence
56    let bundle_sig = pack.okf_dir().join("bundle.sig");
57    if bundle_sig.exists() {
58        checks.push(CheckResult::pass("bundle.sig present"));
59    } else {
60        checks.push(CheckResult::skip("bundle.sig (not present)"));
61    }
62
63    let failed = checks.iter().any(|c| c.status == GateStatus::Fail);
64    GateResult {
65        gate: 8,
66        status: if failed {
67            GateStatus::Fail
68        } else {
69            GateStatus::Pass
70        },
71        checks,
72        message: None,
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use tempfile::TempDir;
80
81    fn minimal_manifest_json() -> &'static str {
82        r#"{
83            "spec": "1.0.0",
84            "name": "test-pack",
85            "version": "1.0.0",
86            "domain": "testing",
87            "audience": "internal",
88            "intended_use": "unit tests",
89            "known_limitations": "none",
90            "update_date": "2026-01-01"
91        }"#
92    }
93
94    fn open_pack(tmp: &TempDir) -> Pack {
95        std::fs::write(tmp.path().join("manifest.json"), minimal_manifest_json()).unwrap();
96        Pack::open(tmp.path()).unwrap()
97    }
98
99    #[test]
100    fn okf_absent_is_not_applicable() {
101        let tmp = TempDir::new().unwrap();
102        let pack = open_pack(&tmp);
103        let result = run(&pack);
104        assert_eq!(result.status, GateStatus::NotApplicable);
105        assert_eq!(result.gate, 8);
106    }
107
108    #[test]
109    fn okf_valid_frontmatter_passes() {
110        let tmp = TempDir::new().unwrap();
111        let pack = open_pack(&tmp);
112        std::fs::create_dir_all(pack.okf_dir()).unwrap();
113        std::fs::write(
114            pack.okf_dir().join("concept.md"),
115            "---\ntype: concept\n---\nBody text.\n",
116        )
117        .unwrap();
118
119        let result = run(&pack);
120        assert_eq!(result.status, GateStatus::Pass);
121    }
122
123    #[test]
124    fn okf_missing_type_field_fails() {
125        let tmp = TempDir::new().unwrap();
126        let pack = open_pack(&tmp);
127        std::fs::create_dir_all(pack.okf_dir()).unwrap();
128        std::fs::write(
129            pack.okf_dir().join("concept.md"),
130            "---\nname: concept\n---\nBody text.\n",
131        )
132        .unwrap();
133
134        let result = run(&pack);
135        assert_eq!(result.status, GateStatus::Fail);
136    }
137
138    #[test]
139    fn okf_unparseable_file_fails() {
140        let tmp = TempDir::new().unwrap();
141        let pack = open_pack(&tmp);
142        std::fs::create_dir_all(pack.okf_dir()).unwrap();
143        std::fs::write(pack.okf_dir().join("concept.md"), "no frontmatter here").unwrap();
144
145        let result = run(&pack);
146        assert_eq!(result.status, GateStatus::Fail);
147    }
148
149    #[test]
150    fn bundle_sig_absent_is_skipped_not_failed() {
151        let tmp = TempDir::new().unwrap();
152        let pack = open_pack(&tmp);
153        std::fs::create_dir_all(pack.okf_dir()).unwrap();
154        std::fs::write(
155            pack.okf_dir().join("concept.md"),
156            "---\ntype: concept\n---\nBody.\n",
157        )
158        .unwrap();
159
160        let result = run(&pack);
161        assert_eq!(result.status, GateStatus::Pass);
162        assert!(result
163            .checks
164            .iter()
165            .any(|c| c.description.contains("bundle.sig") && c.status == GateStatus::Skipped));
166    }
167
168    #[test]
169    fn bundle_sig_present_passes_that_check() {
170        let tmp = TempDir::new().unwrap();
171        let pack = open_pack(&tmp);
172        std::fs::create_dir_all(pack.okf_dir()).unwrap();
173        std::fs::write(
174            pack.okf_dir().join("concept.md"),
175            "---\ntype: concept\n---\nBody.\n",
176        )
177        .unwrap();
178        std::fs::write(pack.okf_dir().join("bundle.sig"), "signature-bytes").unwrap();
179
180        let result = run(&pack);
181        assert_eq!(result.status, GateStatus::Pass);
182        assert!(result
183            .checks
184            .iter()
185            .any(|c| c.description == "bundle.sig present" && c.status == GateStatus::Pass));
186    }
187}