dkp_core/validate/
gate7.rs1use crate::{
2 pack::loader::Pack,
3 validate::gates::{CheckResult, GateResult, GateStatus},
4};
5
6pub fn run(pack: &Pack) -> GateResult {
9 let has_cases = pack.has_eval_set()
10 && pack
11 .load_eval_set()
12 .map(|cases| !cases.is_empty())
13 .unwrap_or(false);
14
15 if !has_cases {
16 return GateResult {
17 gate: 7,
18 status: GateStatus::Skipped,
19 checks: vec![CheckResult::skip("eval_set.jsonl (optional, or empty)")],
20 message: Some("eval_set.jsonl not present or empty; gate 7 skipped".to_string()),
21 };
22 }
23
24 let mut checks = Vec::new();
25
26 match pack.load_eval_set() {
28 Ok(cases) => checks.push(CheckResult::pass(format!(
29 "eval_set.jsonl: {} cases loaded",
30 cases.len()
31 ))),
32 Err(e) => checks.push(CheckResult::fail("eval_set.jsonl parses", e.to_string())),
33 }
34
35 let summary_path = pack
37 .evidence_dir()
38 .join("eval_results")
39 .join("eval_summary.json");
40 if summary_path.exists() {
41 match std::fs::read_to_string(&summary_path)
42 .ok()
43 .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
44 {
45 Some(v) => {
46 let gate7_pass = v["gate7_pass"].as_bool().unwrap_or(false);
47 let mean_delta = v["mean_delta"].as_f64().unwrap_or(0.0);
48 if gate7_pass {
49 checks.push(CheckResult::pass(format!(
50 "eval_summary.json: gate7_pass=true (mean_delta={mean_delta:.3})"
51 )));
52 } else {
53 let min_delta = pack.manifest.min_eval_delta.unwrap_or(0.0);
54 checks.push(CheckResult::fail(
55 "eval_summary.json",
56 format!("gate7_pass=false (mean_delta={mean_delta:.3}, required >= {min_delta:.3})"),
57 ));
58 }
59 }
60 None => checks.push(CheckResult::fail(
61 "eval_summary.json parses",
62 "could not parse evidence/eval_results/eval_summary.json",
63 )),
64 }
65 } else {
66 checks.push(CheckResult::fail(
67 "evidence/eval_results/eval_summary.json",
68 "not found; run `dkp eval` to generate before publishing",
69 ));
70 }
71
72 let failed = checks.iter().any(|c| c.status == GateStatus::Fail);
73 GateResult {
74 gate: 7,
75 status: if failed {
76 GateStatus::Fail
77 } else {
78 GateStatus::Pass
79 },
80 checks,
81 message: None,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use tempfile::TempDir;
89
90 fn minimal_manifest_json() -> &'static str {
91 r#"{
92 "spec": "1.0.0",
93 "name": "test-pack",
94 "version": "1.0.0",
95 "domain": "testing",
96 "audience": "internal",
97 "intended_use": "unit tests",
98 "known_limitations": "none",
99 "update_date": "2026-01-01"
100 }"#
101 }
102
103 fn eval_case_line() -> &'static str {
104 r#"{"query": "q", "expected_dimensions": [], "critical_must_include": [], "scoring_rubric": "r", "version_meta": {"prompt_hash": "h", "model_version": "m", "dataset_version": "d"}}"#
105 }
106
107 fn open_pack(tmp: &TempDir) -> Pack {
108 std::fs::write(tmp.path().join("manifest.json"), minimal_manifest_json()).unwrap();
109 Pack::open(tmp.path()).unwrap()
110 }
111
112 #[test]
113 fn eval_set_absent_is_skipped() {
114 let tmp = TempDir::new().unwrap();
115 let pack = open_pack(&tmp);
116 let result = run(&pack);
117 assert_eq!(result.status, GateStatus::Skipped);
118 assert_eq!(result.gate, 7);
119 }
120
121 #[test]
122 fn eval_set_present_but_empty_is_skipped() {
123 let tmp = TempDir::new().unwrap();
124 let pack = open_pack(&tmp);
125 std::fs::create_dir_all(pack.machine_dir()).unwrap();
126 std::fs::write(pack.machine_file("eval_set.jsonl"), "").unwrap();
127
128 let result = run(&pack);
129 assert_eq!(result.status, GateStatus::Skipped);
130 }
131
132 #[test]
133 fn eval_set_present_but_no_summary_fails() {
134 let tmp = TempDir::new().unwrap();
135 let pack = open_pack(&tmp);
136 std::fs::create_dir_all(pack.machine_dir()).unwrap();
137 std::fs::write(
138 pack.machine_file("eval_set.jsonl"),
139 format!("{}\n", eval_case_line()),
140 )
141 .unwrap();
142
143 let result = run(&pack);
144 assert_eq!(result.status, GateStatus::Fail);
145 assert!(result.checks.iter().any(|c| c
146 .description
147 .contains("evidence/eval_results/eval_summary.json")
148 && c.status == GateStatus::Fail));
149 }
150
151 #[test]
152 fn eval_summary_gate7_pass_true_passes() {
153 let tmp = TempDir::new().unwrap();
154 let pack = open_pack(&tmp);
155 std::fs::create_dir_all(pack.machine_dir()).unwrap();
156 std::fs::write(
157 pack.machine_file("eval_set.jsonl"),
158 format!("{}\n", eval_case_line()),
159 )
160 .unwrap();
161 let eval_results_dir = pack.evidence_dir().join("eval_results");
162 std::fs::create_dir_all(&eval_results_dir).unwrap();
163 std::fs::write(
164 eval_results_dir.join("eval_summary.json"),
165 r#"{"gate7_pass": true, "mean_delta": 0.42}"#,
166 )
167 .unwrap();
168
169 let result = run(&pack);
170 assert_eq!(result.status, GateStatus::Pass);
171 }
172
173 #[test]
174 fn eval_summary_gate7_pass_false_fails() {
175 let tmp = TempDir::new().unwrap();
176 let pack = open_pack(&tmp);
177 std::fs::create_dir_all(pack.machine_dir()).unwrap();
178 std::fs::write(
179 pack.machine_file("eval_set.jsonl"),
180 format!("{}\n", eval_case_line()),
181 )
182 .unwrap();
183 let eval_results_dir = pack.evidence_dir().join("eval_results");
184 std::fs::create_dir_all(&eval_results_dir).unwrap();
185 std::fs::write(
186 eval_results_dir.join("eval_summary.json"),
187 r#"{"gate7_pass": false, "mean_delta": 0.01}"#,
188 )
189 .unwrap();
190
191 let result = run(&pack);
192 assert_eq!(result.status, GateStatus::Fail);
193 }
194
195 #[test]
196 fn eval_summary_unparseable_fails() {
197 let tmp = TempDir::new().unwrap();
198 let pack = open_pack(&tmp);
199 std::fs::create_dir_all(pack.machine_dir()).unwrap();
200 std::fs::write(
201 pack.machine_file("eval_set.jsonl"),
202 format!("{}\n", eval_case_line()),
203 )
204 .unwrap();
205 let eval_results_dir = pack.evidence_dir().join("eval_results");
206 std::fs::create_dir_all(&eval_results_dir).unwrap();
207 std::fs::write(eval_results_dir.join("eval_summary.json"), "not json").unwrap();
208
209 let result = run(&pack);
210 assert_eq!(result.status, GateStatus::Fail);
211 }
212}