Skip to main content

dkp_core/validate/
gate4.rs

1use std::collections::HashSet;
2
3use crate::{
4    pack::loader::Pack,
5    procedures,
6    validate::gates::{CheckResult, GateResult, GateStatus},
7};
8
9const REQUIRED_MACHINE_FILES: &[&str] = &[
10    "system_prompt.md",
11    "glossary.json",
12    "ontology.json",
13    "rules.json",
14    "constraints.json",
15    "retrieval_chunks.jsonl",
16];
17
18/// Gate 4: Machine Usability — required files present, schemas valid, refs resolve.
19pub fn run(pack: &Pack) -> GateResult {
20    let mut checks = Vec::new();
21
22    // Required file presence
23    for file in REQUIRED_MACHINE_FILES {
24        if pack.machine_file(file).exists() {
25            checks.push(CheckResult::pass(format!("machine/{file} present")));
26        } else {
27            checks.push(CheckResult::fail(
28                format!("machine/{file} present"),
29                format!("machine/{file} is required but not found"),
30            ));
31        }
32    }
33
34    // manifest.json present (already opened, so always true if we got here)
35    checks.push(CheckResult::pass("manifest.json present"));
36
37    // Parse each required JSON/JSONL asset — deserialization failure = schema violation
38    for (name, result) in [
39        ("glossary.json", pack.load_glossary().map(|_| ())),
40        ("ontology.json", pack.load_ontology().map(|_| ())),
41        ("rules.json", pack.load_rules().map(|_| ())),
42        ("constraints.json", pack.load_constraints().map(|_| ())),
43        ("retrieval_chunks.jsonl", pack.load_chunks().map(|_| ())),
44    ] {
45        match result {
46            Ok(_) => checks.push(CheckResult::pass(format!("machine/{name} parses"))),
47            Err(e) => checks.push(CheckResult::fail(
48                format!("machine/{name} parses"),
49                e.to_string(),
50            )),
51        }
52    }
53
54    // source_ref resolution against evidence/sources.csv
55    let sources_csv = pack.evidence_file("sources.csv");
56    if sources_csv.exists() {
57        let known_ids: HashSet<String> = std::fs::read_to_string(&sources_csv)
58            .unwrap_or_default()
59            .lines()
60            .skip(1) // header row
61            .filter_map(|line| {
62                line.split(',')
63                    .next()
64                    .map(|s| s.trim().trim_matches('"').to_string())
65            })
66            .filter(|s| !s.is_empty())
67            .collect();
68
69        let mut unresolved: Vec<String> = Vec::new();
70
71        if let Ok(Some(gf)) = pack.load_glossary() {
72            for t in &gf.terms {
73                if let Some(ref sr) = t.source_ref {
74                    if sr != "generated" && !known_ids.contains(sr.as_str()) {
75                        unresolved.push(format!("term/{}: {sr}", t.id));
76                    }
77                }
78            }
79        }
80        if let Ok(Some(rf)) = pack.load_rules() {
81            for r in &rf.rules {
82                if let Some(ref sr) = r.source_ref {
83                    if sr != "generated" && !known_ids.contains(sr.as_str()) {
84                        unresolved.push(format!("rule/{}: {sr}", r.id));
85                    }
86                }
87            }
88        }
89        if let Ok(Some(cf)) = pack.load_constraints() {
90            for c in cf.all_constraints() {
91                if let Some(ref sr) = c.source_ref {
92                    if sr != "generated" && !known_ids.contains(sr.as_str()) {
93                        unresolved.push(format!("constraint/{}: {sr}", c.id));
94                    }
95                }
96            }
97        }
98
99        if unresolved.is_empty() {
100            checks.push(CheckResult::pass("source_ref resolution"));
101        } else {
102            checks.push(CheckResult::fail(
103                "source_ref resolution",
104                format!("unresolved refs: {}", unresolved.join(", ")),
105            ));
106        }
107    }
108
109    // knowledge_graph edge resolution
110    if pack.has_knowledge_graph() {
111        match pack.load_graph() {
112            Ok(Some(graph)) => {
113                let node_ids: HashSet<&str> = graph.nodes.iter().map(|n| n.id.as_str()).collect();
114                let broken: Vec<String> = graph
115                    .edges
116                    .iter()
117                    .filter_map(|e| {
118                        let src_ok = node_ids.contains(e.source.as_str());
119                        let tgt_ok = node_ids.contains(e.target.as_str());
120                        if !src_ok || !tgt_ok {
121                            Some(format!("{}->{}", e.source, e.target))
122                        } else {
123                            None
124                        }
125                    })
126                    .collect();
127                if broken.is_empty() {
128                    checks.push(CheckResult::pass("knowledge_graph edge resolution"));
129                } else {
130                    checks.push(CheckResult::fail(
131                        "knowledge_graph edge resolution",
132                        format!("broken edges: {}", broken.join(", ")),
133                    ));
134                }
135            }
136            Ok(None) => checks.push(CheckResult::skip("knowledge_graph.json (not present)")),
137            Err(e) => checks.push(CheckResult::fail(
138                "knowledge_graph.json parses",
139                e.to_string(),
140            )),
141        }
142    }
143
144    // Procedure completeness when machine/procedures/ is non-empty
145    if pack.has_procedures() {
146        match procedures::validate_all(pack) {
147            Ok(errors) if errors.is_empty() => {
148                checks.push(CheckResult::pass("machine/procedures/ completeness"));
149            }
150            Ok(errors) => {
151                for e in &errors {
152                    checks.push(CheckResult::fail(
153                        "machine/procedures/ completeness",
154                        e.clone(),
155                    ));
156                }
157            }
158            Err(e) => {
159                checks.push(CheckResult::fail(
160                    "machine/procedures/ completeness",
161                    e.to_string(),
162                ));
163            }
164        }
165    }
166
167    // MCP manifest when mcp.enabled
168    if pack.manifest.mcp.as_ref().is_some_and(|m| m.enabled) {
169        if pack.machine_file("mcp_manifest.json").exists() {
170            checks.push(CheckResult::pass(
171                "machine/mcp_manifest.json present (mcp.enabled)",
172            ));
173        } else {
174            checks.push(CheckResult::fail(
175                "machine/mcp_manifest.json present (mcp.enabled)",
176                "manifest.mcp.enabled is true but machine/mcp_manifest.json is missing",
177            ));
178        }
179    }
180
181    let failed = checks.iter().any(|c| c.status == GateStatus::Fail);
182    GateResult {
183        gate: 4,
184        status: if failed {
185            GateStatus::Fail
186        } else {
187            GateStatus::Pass
188        },
189        checks,
190        message: None,
191    }
192}