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;
22
23pub use v3::V3Validator;
24
25/// Outcome of running a single validator against a genome.
26///
27/// `errors` are blocking issues. `warnings` are advisory. The validator does
28/// not decide whether to abort the chain; the chain runner makes that call
29/// based on whether the validator was the latest (blocking) or intermediate
30/// (advisory).
31#[derive(Debug, Clone, Default)]
32pub struct ValidationReport {
33 /// Schema version this report describes. `None` only when constructed
34 /// via `Default` for testing scaffolding; production validators always
35 /// stamp their version.
36 pub schema_version: Option<GenomeSchemaVersion>,
37 pub errors: Vec<String>,
38 pub warnings: Vec<String>,
39}
40
41impl ValidationReport {
42 pub fn new(schema_version: GenomeSchemaVersion) -> Self {
43 Self {
44 schema_version: Some(schema_version),
45 errors: Vec::new(),
46 warnings: Vec::new(),
47 }
48 }
49
50 pub fn add_error(&mut self, msg: impl Into<String>) {
51 self.errors.push(msg.into());
52 }
53
54 pub fn add_warning(&mut self, msg: impl Into<String>) {
55 self.warnings.push(msg.into());
56 }
57
58 /// True when the report carries at least one blocking error.
59 pub fn has_errors(&self) -> bool {
60 !self.errors.is_empty()
61 }
62
63 /// True when the report carries no errors and no warnings.
64 pub fn is_clean(&self) -> bool {
65 self.errors.is_empty() && self.warnings.is_empty()
66 }
67}
68
69/// Validates a genome at a specific schema version.
70///
71/// Validators are stateless; the trait is `Send + Sync` so registries can be
72/// shared across threads. Implementations must not mutate the input or
73/// perform I/O.
74pub trait Validator: Send + Sync {
75 /// The schema version this validator targets.
76 fn schema_version(&self) -> GenomeSchemaVersion;
77
78 /// Inspect a genome and return findings. Never mutates.
79 fn validate(&self, genome: &Value) -> ValidationReport;
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn report_starts_clean() {
88 let r = ValidationReport::new(GenomeSchemaVersion(3));
89 assert!(r.is_clean());
90 assert!(!r.has_errors());
91 assert_eq!(r.schema_version, Some(GenomeSchemaVersion(3)));
92 }
93
94 #[test]
95 fn add_error_breaks_clean_and_blocks() {
96 let mut r = ValidationReport::new(GenomeSchemaVersion(3));
97 r.add_error("missing field");
98 assert!(r.has_errors());
99 assert!(!r.is_clean());
100 assert_eq!(r.errors, vec!["missing field".to_string()]);
101 }
102
103 #[test]
104 fn warning_is_advisory_only() {
105 let mut r = ValidationReport::new(GenomeSchemaVersion(3));
106 r.add_warning("nudge");
107 assert!(!r.has_errors());
108 assert!(!r.is_clean());
109 assert_eq!(r.warnings, vec!["nudge".to_string()]);
110 }
111}