feagi_evolutionary/genome/validators/mod.rs
1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Per-version genome validators.
5//!
6//! A `Validator` checks structural integrity, parameter ranges, and
7//! cross-references at a *specific* `GenomeSchemaVersion`. The chain runner
8//! invokes per-version validators between hops as **advisory** and the
9//! validator at the final target version as **blocking**.
10//!
11//! See `feagi-core/docs/GENOME_SCHEMA_VERSIONING.md` and
12//! `crates/feagi-evolutionary/src/genome/README.md` for the design contract.
13//!
14//! Validators MUST NOT mutate the genome. Mutation belongs in
15//! `crate::genome::migration::Migrator`. See the README's anti-patterns.
16
17use serde_json::Value;
18
19use crate::genome::schema::GenomeSchemaVersion;
20
21pub mod v3;
22pub mod v4;
23
24pub use v3::V3Validator;
25pub use v4::V4Validator;
26
27/// Outcome of running a single validator against a genome.
28///
29/// `errors` are blocking issues. `warnings` are advisory. The validator does
30/// not decide whether to abort the chain; the chain runner makes that call
31/// based on whether the validator was the latest (blocking) or intermediate
32/// (advisory).
33#[derive(Debug, Clone, Default)]
34pub struct ValidationReport {
35 /// Schema version this report describes. `None` only when constructed
36 /// via `Default` for testing scaffolding; production validators always
37 /// stamp their version.
38 pub schema_version: Option<GenomeSchemaVersion>,
39 pub errors: Vec<String>,
40 pub warnings: Vec<String>,
41}
42
43impl ValidationReport {
44 pub fn new(schema_version: GenomeSchemaVersion) -> Self {
45 Self {
46 schema_version: Some(schema_version),
47 errors: Vec::new(),
48 warnings: Vec::new(),
49 }
50 }
51
52 pub fn add_error(&mut self, msg: impl Into<String>) {
53 self.errors.push(msg.into());
54 }
55
56 pub fn add_warning(&mut self, msg: impl Into<String>) {
57 self.warnings.push(msg.into());
58 }
59
60 /// True when the report carries at least one blocking error.
61 pub fn has_errors(&self) -> bool {
62 !self.errors.is_empty()
63 }
64
65 /// True when the report carries no errors and no warnings.
66 pub fn is_clean(&self) -> bool {
67 self.errors.is_empty() && self.warnings.is_empty()
68 }
69}
70
71/// Validates a genome at a specific schema version.
72///
73/// Validators are stateless; the trait is `Send + Sync` so registries can be
74/// shared across threads. Implementations must not mutate the input or
75/// perform I/O.
76pub trait Validator: Send + Sync {
77 /// The schema version this validator targets.
78 fn schema_version(&self) -> GenomeSchemaVersion;
79
80 /// Inspect a genome and return findings. Never mutates.
81 fn validate(&self, genome: &Value) -> ValidationReport;
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn report_starts_clean() {
90 let r = ValidationReport::new(GenomeSchemaVersion(3));
91 assert!(r.is_clean());
92 assert!(!r.has_errors());
93 assert_eq!(r.schema_version, Some(GenomeSchemaVersion(3)));
94 }
95
96 #[test]
97 fn add_error_breaks_clean_and_blocks() {
98 let mut r = ValidationReport::new(GenomeSchemaVersion(3));
99 r.add_error("missing field");
100 assert!(r.has_errors());
101 assert!(!r.is_clean());
102 assert_eq!(r.errors, vec!["missing field".to_string()]);
103 }
104
105 #[test]
106 fn warning_is_advisory_only() {
107 let mut r = ValidationReport::new(GenomeSchemaVersion(3));
108 r.add_warning("nudge");
109 assert!(!r.has_errors());
110 assert!(!r.is_clean());
111 assert_eq!(r.warnings, vec!["nudge".to_string()]);
112 }
113}