dasein_agentic_core/distributed/
validator.rs1use serde::{Deserialize, Serialize};
37
38use super::config::ValidatorConfig;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum ValidationRule {
44 OutputNotEmpty,
46 NoErrors,
48 ValidJson,
50 CodeCompiles { language: String },
52 NoTodos,
54 HasTests,
56 NoSecrets,
58 MaxLength { chars: usize },
60 MinLength { chars: usize },
62 Contains { pattern: String },
64 NotContains { pattern: String },
66 Custom { name: String },
68}
69
70impl ValidationRule {
71 pub fn name(&self) -> &str {
73 match self {
74 Self::OutputNotEmpty => "output_not_empty",
75 Self::NoErrors => "no_errors",
76 Self::ValidJson => "valid_json",
77 Self::CodeCompiles { .. } => "code_compiles",
78 Self::NoTodos => "no_todos",
79 Self::HasTests => "has_tests",
80 Self::NoSecrets => "no_secrets",
81 Self::MaxLength { .. } => "max_length",
82 Self::MinLength { .. } => "min_length",
83 Self::Contains { .. } => "contains",
84 Self::NotContains { .. } => "not_contains",
85 Self::Custom { name } => name,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ValidationResult {
93 pub passed: bool,
95 pub rule_results: Vec<RuleResult>,
97 pub score: u32,
99 pub feedback: Option<String>,
101 pub action: ValidationAction,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct RuleResult {
108 pub rule: String,
109 pub passed: bool,
110 pub message: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum ValidationAction {
117 Accept,
119 Retry { feedback: String, attempt: u32 },
121 Reject { reason: String },
123}
124
125#[derive(Debug)]
127pub struct Validator {
128 pub id: String,
130 pub supervisor: String,
132 rules: Vec<ValidationRule>,
134 max_retries: u32,
136}
137
138impl Validator {
139 pub fn new(id: impl Into<String>, supervisor: impl Into<String>) -> ValidatorBuilder {
149 ValidatorBuilder::new(id.into(), supervisor.into())
150 }
151
152 pub fn from_config(config: ValidatorConfig) -> Self {
154 let rules = config
155 .rules
156 .iter()
157 .map(|r| match r.as_str() {
158 "output_not_empty" => ValidationRule::OutputNotEmpty,
159 "no_errors" => ValidationRule::NoErrors,
160 "valid_json" => ValidationRule::ValidJson,
161 "no_todos" => ValidationRule::NoTodos,
162 "has_tests" => ValidationRule::HasTests,
163 "no_secrets" => ValidationRule::NoSecrets,
164 _ => ValidationRule::Custom { name: r.clone() },
165 })
166 .collect();
167
168 Self {
169 id: config.id,
170 supervisor: config.supervisor,
171 rules,
172 max_retries: config.max_retries,
173 }
174 }
175
176 pub fn validate(&self, output: &str, attempt: u32) -> ValidationResult {
178 let mut rule_results = Vec::new();
179 let mut all_passed = true;
180 let mut feedback_parts = Vec::new();
181
182 for rule in &self.rules {
183 let (passed, message) = self.check_rule(rule, output);
184
185 if !passed {
186 all_passed = false;
187 if let Some(ref msg) = message {
188 feedback_parts.push(format!("{}: {}", rule.name(), msg));
189 }
190 }
191
192 rule_results.push(RuleResult {
193 rule: rule.name().to_string(),
194 passed,
195 message,
196 });
197 }
198
199 let passed_count = rule_results.iter().filter(|r| r.passed).count();
200 let score = if self.rules.is_empty() {
201 100
202 } else {
203 ((passed_count as f32 / self.rules.len() as f32) * 100.0) as u32
204 };
205
206 let action = if all_passed {
207 ValidationAction::Accept
208 } else if attempt < self.max_retries {
209 ValidationAction::Retry {
210 feedback: feedback_parts.join("; "),
211 attempt: attempt + 1,
212 }
213 } else {
214 ValidationAction::Reject {
215 reason: feedback_parts.join("; "),
216 }
217 };
218
219 let feedback = if all_passed {
220 None
221 } else {
222 Some(feedback_parts.join("; "))
223 };
224
225 ValidationResult {
226 passed: all_passed,
227 rule_results,
228 score,
229 feedback,
230 action,
231 }
232 }
233
234 fn check_rule(&self, rule: &ValidationRule, output: &str) -> (bool, Option<String>) {
236 match rule {
237 ValidationRule::OutputNotEmpty => {
238 let passed = !output.trim().is_empty();
239 (
240 passed,
241 if passed {
242 None
243 } else {
244 Some("Output is empty".into())
245 },
246 )
247 }
248 ValidationRule::NoErrors => {
249 let has_error = output.to_lowercase().contains("error:");
250 (
251 !has_error,
252 if has_error {
253 Some("Output contains errors".into())
254 } else {
255 None
256 },
257 )
258 }
259 ValidationRule::ValidJson => match serde_json::from_str::<serde_json::Value>(output) {
260 Ok(_) => (true, None),
261 Err(e) => (false, Some(format!("Invalid JSON: {}", e))),
262 },
263 ValidationRule::NoTodos => {
264 let has_todo = output.contains("TODO") || output.contains("FIXME");
265 (
266 !has_todo,
267 if has_todo {
268 Some("Contains TODO/FIXME".into())
269 } else {
270 None
271 },
272 )
273 }
274 ValidationRule::HasTests => {
275 let has_tests = output.contains("#[test]") || output.contains("fn test_");
276 (
277 has_tests,
278 if has_tests {
279 None
280 } else {
281 Some("No tests found".into())
282 },
283 )
284 }
285 ValidationRule::NoSecrets => {
286 let patterns = ["api_key", "secret", "password", "token"];
287 let has_secret = patterns.iter().any(|p| output.to_lowercase().contains(p));
288 (
289 !has_secret,
290 if has_secret {
291 Some("Potential secret detected".into())
292 } else {
293 None
294 },
295 )
296 }
297 ValidationRule::MaxLength { chars } => {
298 let passed = output.len() <= *chars;
299 (
300 passed,
301 if passed {
302 None
303 } else {
304 Some(format!("Output too long: {} > {}", output.len(), chars))
305 },
306 )
307 }
308 ValidationRule::MinLength { chars } => {
309 let passed = output.len() >= *chars;
310 (
311 passed,
312 if passed {
313 None
314 } else {
315 Some(format!("Output too short: {} < {}", output.len(), chars))
316 },
317 )
318 }
319 ValidationRule::Contains { pattern } => {
320 let passed = output.contains(pattern);
321 (
322 passed,
323 if passed {
324 None
325 } else {
326 Some(format!("Missing required pattern: {}", pattern))
327 },
328 )
329 }
330 ValidationRule::NotContains { pattern } => {
331 let passed = !output.contains(pattern);
332 (
333 passed,
334 if passed {
335 None
336 } else {
337 Some(format!("Contains forbidden pattern: {}", pattern))
338 },
339 )
340 }
341 ValidationRule::CodeCompiles { language: _ } => {
342 (true, None)
344 }
345 ValidationRule::Custom { name: _ } => {
346 (true, None)
348 }
349 }
350 }
351}
352
353pub struct ValidatorBuilder {
355 id: String,
356 supervisor: String,
357 rules: Vec<ValidationRule>,
358 max_retries: u32,
359}
360
361impl ValidatorBuilder {
362 fn new(id: String, supervisor: String) -> Self {
363 Self {
364 id,
365 supervisor,
366 rules: Vec::new(),
367 max_retries: 2,
368 }
369 }
370
371 pub fn rule(mut self, rule: ValidationRule) -> Self {
373 self.rules.push(rule);
374 self
375 }
376
377 pub fn rules(mut self, rules: impl IntoIterator<Item = ValidationRule>) -> Self {
379 self.rules.extend(rules);
380 self
381 }
382
383 pub fn default_rules(self) -> Self {
385 self.rule(ValidationRule::OutputNotEmpty)
386 .rule(ValidationRule::NoErrors)
387 }
388
389 pub fn strict_code_rules(self) -> Self {
391 self.rule(ValidationRule::OutputNotEmpty)
392 .rule(ValidationRule::NoErrors)
393 .rule(ValidationRule::NoTodos)
394 .rule(ValidationRule::NoSecrets)
395 }
396
397 pub fn max_retries(mut self, n: u32) -> Self {
399 self.max_retries = n;
400 self
401 }
402
403 pub fn build(self) -> Validator {
405 let rules = if self.rules.is_empty() {
406 vec![ValidationRule::OutputNotEmpty]
407 } else {
408 self.rules
409 };
410
411 Validator {
412 id: self.id,
413 supervisor: self.supervisor,
414 rules,
415 max_retries: self.max_retries,
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_validator_pass() {
426 let val = Validator::new("val-001", "sup-001")
427 .rule(ValidationRule::OutputNotEmpty)
428 .rule(ValidationRule::NoErrors)
429 .build();
430
431 let result = val.validate("Hello world", 0);
432 assert!(result.passed);
433 assert_eq!(result.score, 100);
434 }
435
436 #[test]
437 fn test_validator_fail_empty() {
438 let val = Validator::new("val-001", "sup-001")
439 .rule(ValidationRule::OutputNotEmpty)
440 .build();
441
442 let result = val.validate("", 0);
443 assert!(!result.passed);
444 assert!(matches!(result.action, ValidationAction::Retry { .. }));
445 }
446
447 #[test]
448 fn test_validator_fail_max_retries() {
449 let val = Validator::new("val-001", "sup-001")
450 .rule(ValidationRule::OutputNotEmpty)
451 .max_retries(2)
452 .build();
453
454 let result = val.validate("", 2);
455 assert!(!result.passed);
456 assert!(matches!(result.action, ValidationAction::Reject { .. }));
457 }
458}