feagi_evolutionary/genome/normalizers/mod.rs
1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Per-version genome normalizers.
5//!
6//! A `Normalizer` cleans up well-known bad values *within* a single schema
7//! version. It is structurally distinct from a `Migrator`, which advances
8//! the schema version. The chain runner invokes the normalizer for each
9//! "arrived at" version (post-hop or starting version when no migration is
10//! needed) before running that version's validator.
11//!
12//! Normalizers MUST follow the same invariants as migrators:
13//! determinism, idempotence, bounded compute, no side channels, JSON only,
14//! diagnostics over silence. They MUST NOT touch `genome_schema_version`;
15//! the runner is the single source of truth for version bookkeeping.
16//!
17//! See `feagi-core/docs/GENOME_SCHEMA_VERSIONING.md` and
18//! `crates/feagi-evolutionary/src/genome/README.md`.
19
20use serde_json::Value;
21
22use crate::genome::migration::MigrationError;
23use crate::genome::schema::GenomeSchemaVersion;
24
25pub mod v3;
26
27pub use v3::V3Normalizer;
28
29/// Diagnostic record produced by a single normalizer pass.
30///
31/// Every correction the normalizer performs MUST contribute at least one
32/// entry to `transformations`. A normalizer that runs and produces zero
33/// diagnostics on a genome that needed corrections is a bug.
34#[derive(Debug, Clone)]
35pub struct NormalizationDiagnostics {
36 pub schema_version: GenomeSchemaVersion,
37 pub transformations: Vec<String>,
38}
39
40impl NormalizationDiagnostics {
41 pub fn new(schema_version: GenomeSchemaVersion) -> Self {
42 Self {
43 schema_version,
44 transformations: Vec::new(),
45 }
46 }
47
48 pub fn record(&mut self, msg: impl Into<String>) {
49 self.transformations.push(msg.into());
50 }
51
52 /// True when no corrections were applied.
53 pub fn is_clean(&self) -> bool {
54 self.transformations.is_empty()
55 }
56}
57
58/// In-place cleanup of a genome at a specific schema version.
59///
60/// Reuses `MigrationError` for failure reporting because normalizers and
61/// migrators raise structurally identical errors (step name, version,
62/// reason). Adding a parallel error enum would only duplicate the variants.
63pub trait Normalizer: Send + Sync {
64 /// Schema version this normalizer targets.
65 fn schema_version(&self) -> GenomeSchemaVersion;
66
67 /// Stable identifier for diagnostics and logs.
68 fn name(&self) -> &'static str;
69
70 /// Apply corrections in place. Returns diagnostics describing what
71 /// changed, or an error on hard failure.
72 fn normalize(&self, genome: &mut Value) -> Result<NormalizationDiagnostics, MigrationError>;
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn diagnostics_starts_clean() {
81 let d = NormalizationDiagnostics::new(GenomeSchemaVersion(3));
82 assert!(d.is_clean());
83 assert_eq!(d.transformations.len(), 0);
84 assert_eq!(d.schema_version, GenomeSchemaVersion(3));
85 }
86
87 #[test]
88 fn record_breaks_clean() {
89 let mut d = NormalizationDiagnostics::new(GenomeSchemaVersion(3));
90 d.record("set width 0 -> 1");
91 assert!(!d.is_clean());
92 assert_eq!(d.transformations, vec!["set width 0 -> 1".to_string()]);
93 }
94}