eval_magic/validation/
batch.rs1use std::fs;
8use std::io;
9use std::path::Path;
10
11use crate::validation::evals::validate_evals_config;
12
13#[derive(Debug, Clone)]
15pub struct FileOutcome {
16 pub skill: String,
18 pub rel_path: String,
20 pub error: Option<String>,
22}
23
24#[derive(Debug, Default)]
26pub struct ValidationReport {
27 pub outcomes: Vec<FileOutcome>,
28}
29
30impl ValidationReport {
31 pub fn validated(&self) -> usize {
33 self.outcomes.iter().filter(|o| o.error.is_none()).count()
34 }
35
36 pub fn failed(&self) -> usize {
38 self.outcomes.iter().filter(|o| o.error.is_some()).count()
39 }
40}
41
42pub fn validate_all(skill_dir: &Path) -> io::Result<ValidationReport> {
48 let mut skills: Vec<String> = fs::read_dir(skill_dir)?
51 .filter_map(|entry| {
52 let entry = entry.ok()?;
53 entry
54 .path()
55 .is_dir()
56 .then(|| entry.file_name().to_string_lossy().into_owned())
57 })
58 .collect();
59 skills.sort();
60
61 let mut report = ValidationReport::default();
62 for skill in skills {
63 let evals_path = skill_dir.join(&skill).join("evals").join("evals.json");
64 if !evals_path.exists() {
65 continue;
66 }
67
68 let rel_path = format!("{skill}/evals/evals.json");
69 let error = match fs::read_to_string(&evals_path) {
70 Err(e) => Some(format!("{rel_path}: {e}")),
71 Ok(contents) => match serde_json::from_str(&contents) {
72 Err(e) => Some(format!("{rel_path}: invalid JSON: {e}")),
73 Ok(raw) => validate_evals_config(&raw, &rel_path)
74 .err()
75 .map(|e| e.to_string()),
76 },
77 };
78
79 report.outcomes.push(FileOutcome {
80 skill,
81 rel_path,
82 error,
83 });
84 }
85
86 Ok(report)
87}
88
89pub fn validate_one(skill_subdir: &Path) -> io::Result<ValidationReport> {
92 let skill = skill_subdir
93 .file_name()
94 .map(|name| name.to_string_lossy().into_owned())
95 .unwrap_or_else(|| "skill".to_string());
96 let evals_path = skill_subdir.join("evals").join("evals.json");
97 let rel_path = "evals/evals.json".to_string();
98 let error = match fs::read_to_string(&evals_path) {
99 Err(e) => Some(format!("{rel_path}: {e}")),
100 Ok(contents) => match serde_json::from_str(&contents) {
101 Err(e) => Some(format!("{rel_path}: invalid JSON: {e}")),
102 Ok(raw) => validate_evals_config(&raw, &rel_path)
103 .err()
104 .map(|e| e.to_string()),
105 },
106 };
107
108 Ok(ValidationReport {
109 outcomes: vec![FileOutcome {
110 skill,
111 rel_path,
112 error,
113 }],
114 })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::validate_all;
120 use std::fs;
121 use tempfile::TempDir;
122
123 const VALID: &str = r#"{ "skill_name": "demo", "evals": [
125 { "id": "e1", "prompt": "p", "expected_output": "o" } ] }"#;
126
127 fn write_evals(root: &std::path::Path, skill: &str, contents: &str) {
129 let dir = root.join(skill).join("evals");
130 fs::create_dir_all(&dir).unwrap();
131 fs::write(dir.join("evals.json"), contents).unwrap();
132 }
133
134 #[test]
135 fn reports_one_outcome_per_skill_with_an_evals_file() {
136 let tmp = TempDir::new().unwrap();
137 let root = tmp.path();
138 write_evals(root, "good", VALID);
139 write_evals(root, "bad", r#"{ "skill_name": "x", "evals": [] }"#); write_evals(root, "broken", "{ not json"); fs::create_dir_all(root.join("nofile")).unwrap();
143 fs::write(root.join("README.md"), "hi").unwrap();
145
146 let report = validate_all(root).unwrap();
147
148 assert_eq!(report.validated(), 1);
149 assert_eq!(report.failed(), 2);
150
151 let good = report.outcomes.iter().find(|o| o.skill == "good").unwrap();
152 assert!(good.error.is_none());
153 assert_eq!(good.rel_path, "good/evals/evals.json");
154
155 let bad = report.outcomes.iter().find(|o| o.skill == "bad").unwrap();
156 assert!(bad.error.is_some());
157
158 let broken = report
159 .outcomes
160 .iter()
161 .find(|o| o.skill == "broken")
162 .unwrap();
163 assert!(broken.error.is_some());
164
165 assert!(report.outcomes.iter().all(|o| o.skill != "nofile"));
166 assert!(report.outcomes.iter().all(|o| o.skill != "README.md"));
167 }
168
169 #[test]
170 fn an_all_valid_dir_has_no_failures() {
171 let tmp = TempDir::new().unwrap();
172 write_evals(tmp.path(), "a", VALID);
173 write_evals(tmp.path(), "b", VALID);
174
175 let report = validate_all(tmp.path()).unwrap();
176
177 assert_eq!(report.validated(), 2);
178 assert_eq!(report.failed(), 0);
179 assert!(report.outcomes.iter().all(|o| o.error.is_none()));
180 }
181}