Skip to main content

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}