dkp_core/validate/
gate4.rs1use 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
18pub fn run(pack: &Pack) -> GateResult {
20 let mut checks = Vec::new();
21
22 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 checks.push(CheckResult::pass("manifest.json present"));
36
37 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 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) .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 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 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 if pack.manifest.mcp.is_some() {
169 if pack.machine_file("mcp_manifest.json").exists() {
170 checks.push(CheckResult::pass(
171 "machine/mcp_manifest.json present (mcp configured)",
172 ));
173 } else {
174 checks.push(CheckResult::fail(
175 "machine/mcp_manifest.json present (mcp configured)",
176 "manifest.mcp is set 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}