Skip to main content

feagi_evolutionary/
validator.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Genome validation for FEAGI.
6
7Validates genome structure, morphologies, parameters, and constraints.
8Provides clear error messages for debugging.
9
10Copyright 2025 Neuraville Inc.
11Licensed under the Apache License, Version 2.0
12*/
13
14use crate::{MorphologyParameters, RuntimeGenome};
15// CorticalID is used in function signatures but may appear unused in some contexts
16#[allow(unused_imports)]
17use feagi_structures::genomic::cortical_area::CorticalID;
18use serde_json::Value;
19use std::collections::HashSet;
20use std::str::FromStr;
21
22/// Validation result
23#[derive(Debug, Clone)]
24pub struct ValidationResult {
25    /// Whether the genome is valid
26    pub valid: bool,
27    /// List of errors (blocking issues)
28    pub errors: Vec<String>,
29    /// List of warnings (non-blocking issues)
30    pub warnings: Vec<String>,
31}
32
33impl ValidationResult {
34    /// Create a new valid result
35    pub fn new() -> Self {
36        Self {
37            valid: true,
38            errors: Vec::new(),
39            warnings: Vec::new(),
40        }
41    }
42
43    /// Add an error
44    pub fn add_error(&mut self, error: String) {
45        self.valid = false;
46        self.errors.push(error);
47    }
48
49    /// Add a warning
50    pub fn add_warning(&mut self, warning: String) {
51        self.warnings.push(warning);
52    }
53
54    /// Merge another validation result into this one
55    pub fn merge(&mut self, other: ValidationResult) {
56        if !other.valid {
57            self.valid = false;
58        }
59        self.errors.extend(other.errors);
60        self.warnings.extend(other.warnings);
61    }
62}
63
64impl Default for ValidationResult {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70/// Validate a RuntimeGenome
71pub fn validate_genome(genome: &RuntimeGenome) -> ValidationResult {
72    let mut result = ValidationResult::new();
73
74    // Validate metadata
75    validate_metadata(genome, &mut result);
76
77    // Validate cortical areas
78    validate_cortical_areas(genome, &mut result);
79
80    // Validate morphologies
81    validate_morphologies(genome, &mut result);
82
83    // Validate physiology
84    validate_physiology(genome, &mut result);
85
86    // Cross-validate (e.g., check references between sections)
87    cross_validate(genome, &mut result);
88
89    result
90}
91
92/// Auto-fix common genome issues (zero dimensions, zero per_voxel_neuron_cnt, missing physiology)
93///
94/// This function modifies the genome in-place to fix issues that can be automatically corrected.
95/// Should be called before validation to prevent common user errors.
96///
97/// # Arguments
98/// * `genome` - Mutable reference to genome to fix
99///
100/// # Returns
101/// * Number of fixes applied
102pub fn auto_fix_genome(genome: &mut RuntimeGenome) -> usize {
103    use tracing::info;
104
105    let mut fixes_applied = 0;
106
107    // Fix missing or invalid physiology values
108    if genome.physiology.simulation_timestep <= 0.0 {
109        let default_timestep = crate::runtime::PhysiologyConfig::default().simulation_timestep;
110        info!(
111            "🔧 AUTO-FIX: Invalid simulation_timestep {} → {} (default)",
112            genome.physiology.simulation_timestep, default_timestep
113        );
114        genome.physiology.simulation_timestep = default_timestep;
115        fixes_applied += 1;
116    }
117
118    if genome.physiology.max_age == 0 {
119        let default_age = crate::runtime::PhysiologyConfig::default().max_age;
120        info!("🔧 AUTO-FIX: max_age 0 → {} (default)", default_age);
121        genome.physiology.max_age = default_age;
122        fixes_applied += 1;
123    }
124
125    // Fix missing or invalid quantization_precision
126    if genome.physiology.quantization_precision.is_empty() {
127        let default_precision = crate::runtime::default_quantization_precision();
128        info!(
129            "🔧 AUTO-FIX: Missing quantization_precision → '{}' (default)",
130            default_precision
131        );
132        genome.physiology.quantization_precision = default_precision;
133        fixes_applied += 1;
134    } else {
135        // Normalize to canonical format
136        use feagi_npu_neural::types::Precision;
137        match Precision::from_str(&genome.physiology.quantization_precision) {
138            Ok(precision) => {
139                let canonical = precision.as_str().to_string();
140                if genome.physiology.quantization_precision != canonical {
141                    info!(
142                        "🔧 AUTO-FIX: Quantization precision '{}' → '{}' (normalized)",
143                        genome.physiology.quantization_precision, canonical
144                    );
145                    genome.physiology.quantization_precision = canonical;
146                    fixes_applied += 1;
147                }
148            }
149            Err(_) => {
150                // Invalid precision - will be caught by validator
151                let default_precision = crate::runtime::default_quantization_precision();
152                info!(
153                    "🔧 AUTO-FIX: Invalid quantization_precision '{}' → '{}' (default)",
154                    genome.physiology.quantization_precision, default_precision
155                );
156                genome.physiology.quantization_precision = default_precision;
157                fixes_applied += 1;
158            }
159        }
160    }
161
162    for (cortical_id, area) in &mut genome.cortical_areas {
163        let cortical_id_display = cortical_id.to_string();
164        // Fix zero dimensions
165        if area.dimensions.width == 0 {
166            info!(
167                "🔧 AUTO-FIX: Cortical area '{}' width 0 → 1",
168                cortical_id_display
169            );
170            area.dimensions.width = 1;
171            fixes_applied += 1;
172        }
173        if area.dimensions.height == 0 {
174            info!(
175                "🔧 AUTO-FIX: Cortical area '{}' height 0 → 1",
176                cortical_id_display
177            );
178            area.dimensions.height = 1;
179            fixes_applied += 1;
180        }
181        if area.dimensions.depth == 0 {
182            info!(
183                "🔧 AUTO-FIX: Cortical area '{}' depth 0 → 1",
184                cortical_id_display
185            );
186            area.dimensions.depth = 1;
187            fixes_applied += 1;
188        }
189
190        // Fix zero neurons_per_voxel (stored in properties)
191        let neurons_per_voxel = area
192            .properties
193            .get("neurons_per_voxel")
194            .and_then(|v| v.as_u64())
195            .unwrap_or(0) as u32;
196        if neurons_per_voxel == 0 {
197            info!(
198                "🔧 AUTO-FIX: Cortical area '{}' neurons_per_voxel 0 → 1",
199                cortical_id_display
200            );
201            area.properties
202                .insert("neurons_per_voxel".to_string(), serde_json::json!(1));
203            fixes_applied += 1;
204        }
205    }
206
207    if fixes_applied > 0 {
208        info!(
209            "🔧 AUTO-FIX: Applied {} automatic corrections to genome",
210            fixes_applied
211        );
212    }
213
214    fixes_applied
215}
216
217/// Validate genome metadata
218fn validate_metadata(genome: &RuntimeGenome, result: &mut ValidationResult) {
219    if genome.metadata.genome_id.is_empty() {
220        result.add_error("Genome ID is empty".to_string());
221    }
222
223    if genome.metadata.version.is_empty() {
224        result.add_error("Genome version is empty".to_string());
225    }
226
227    if genome.metadata.version != "2.0" {
228        result.add_warning(format!(
229            "Genome version '{}' may not be fully supported (expected '2.0')",
230            genome.metadata.version
231        ));
232    }
233}
234
235/// Validate cortical areas
236fn validate_cortical_areas(genome: &RuntimeGenome, result: &mut ValidationResult) {
237    if genome.cortical_areas.is_empty() {
238        result.add_warning("Genome has no cortical areas defined".to_string());
239        return;
240    }
241
242    for (cortical_id, area) in &genome.cortical_areas {
243        let cortical_id_display = cortical_id.to_string();
244
245        // CRITICAL: Validate cortical ID format and compliance with feagi-data-processing templates
246        validate_cortical_id_format(cortical_id, &cortical_id_display, result);
247
248        // Validate dimensions - AUTO-FIX zeros to 1
249        if area.dimensions.width == 0 || area.dimensions.height == 0 || area.dimensions.depth == 0 {
250            result.add_warning(format!(
251                "AUTO-FIX: Cortical area '{}' has zero dimension(s): {}x{}x{} - will be corrected to minimum (1,1,1)",
252                cortical_id_display, area.dimensions.width, area.dimensions.height, area.dimensions.depth
253            ));
254            // Note: Auto-fix happens in auto_fix_genome() - this just detects the issue
255        }
256
257        // Validate neurons_per_voxel (stored in properties)
258        let neurons_per_voxel = area
259            .properties
260            .get("neurons_per_voxel")
261            .and_then(|v| v.as_u64())
262            .unwrap_or(0) as u32;
263        if neurons_per_voxel == 0 {
264            result.add_warning(format!(
265                "AUTO-FIX: Cortical area '{}' has neurons_per_voxel=0 - will be corrected to 1",
266                cortical_id_display
267            ));
268        }
269
270        // Warn about very large dimensions
271        let total_voxels = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
272        if total_voxels > 1_000_000 {
273            result.add_warning(format!(
274                "Cortical area '{}' has very large dimensions: {} total voxels",
275                cortical_id_display, total_voxels
276            ));
277        }
278
279        // Validate name
280        if area.name.is_empty() {
281            result.add_warning(format!(
282                "Cortical area '{}' has empty name",
283                cortical_id_display
284            ));
285        }
286    }
287}
288
289/// Validate cortical ID format and compliance with feagi-data-processing templates
290fn validate_cortical_id_format(
291    _cortical_id: &CorticalID,
292    display: &str,
293    result: &mut ValidationResult,
294) {
295    // Base64 encoded 8-byte IDs are 12 characters (with padding)
296    // Old format IDs are 8 characters
297    // Accept both formats for backward compatibility
298    if display.len() != 8 && display.len() != 12 {
299        result.add_error(format!(
300            "Invalid cortical ID length: '{}' is {} characters (must be 8 or 12)",
301            display,
302            display.len()
303        ));
304        return;
305    }
306
307    // Check if it's a CORE area (starts with underscore)
308    if display.starts_with('_') {
309        validate_core_area_id(display, result);
310        return;
311    }
312
313    // Check if it's a CUSTOM/MEMORY area (starts with 'c')
314    if display.starts_with('c') {
315        // Custom areas: No strict validation yet, but should follow naming conventions
316        // Just check that it's properly padded
317        if !display.chars().all(|c| c.is_alphanumeric() || c == '_') {
318            result.add_warning(format!(
319                "Custom cortical ID '{}' contains non-alphanumeric characters",
320                display
321            ));
322        }
323        return;
324    }
325
326    // Check if it's an IPU/OPU area (3-char prefix + 5 chars)
327    validate_io_area_id(display, result);
328}
329
330/// Validate CORE area IDs (power, death, etc.) using feagi-data-processing types
331fn validate_core_area_id(display: &str, result: &mut ValidationResult) {
332    use feagi_structures::genomic::cortical_area::CoreCorticalType;
333
334    // Generate valid CORE IDs from the authoritative source (feagi-data-processing)
335    let valid_core_ids: Vec<String> = vec![
336        CoreCorticalType::Power.to_cortical_id().to_string(), // "___power"
337        CoreCorticalType::Death.to_cortical_id().to_string(), // "___death"
338        CoreCorticalType::Fatigue.to_cortical_id().to_string(), // "___fatig"
339        CoreCorticalType::Pain.to_cortical_id().to_string(),  // "___pain_"
340        CoreCorticalType::Pleasure.to_cortical_id().to_string(), // "___pleas"
341        CoreCorticalType::Fear.to_cortical_id().to_string(),  // "___fear_"
342        CoreCorticalType::Hope.to_cortical_id().to_string(),  // "___hope_"
343    ];
344
345    if !valid_core_ids.contains(&display.to_string()) {
346        result.add_error(format!(
347            "Invalid CORE cortical ID: '{}' - must be one of: {:?}",
348            display, valid_core_ids
349        ));
350    }
351}
352
353/// Validate IPU/OPU area IDs (should follow template system)
354fn validate_io_area_id(display: &str, result: &mut ValidationResult) {
355    // IO cortical IDs have format: [i/o][3-char-unit][4-config-bytes]
356    // For IPU: 'i' + 3-char prefix (e.g., "isvi____")
357    // For OPU: 'o' + 3-char prefix (e.g., "omot____")
358    let first_char = display.chars().next().unwrap_or('_');
359    let unit_prefix = &display[1..4]; // Skip first char (i/o), get 3-char unit identifier
360
361    // Known valid IPU prefixes from feagi-data-processing templates
362    const VALID_IPU_PREFIXES: &[&str] = &[
363        "svi", // SegmentedVision (9 areas: isvi____ variants)
364        "aud", // Audio
365        "tac", // Tactile
366        "olf", // Olfactory
367        "vis", // Vision (generic)
368    ];
369
370    // Known valid OPU prefixes from feagi-data-processing templates
371    const VALID_OPU_PREFIXES: &[&str] = &[
372        "mot", // Motor (omot____ variants)
373        "voc", // Vocal
374        "gaz", // Gaze control
375        "pse", // Positional Servo
376        "mis", // Miscellaneous
377    ];
378
379    let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
380    let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
381
382    if !is_valid_ipu && !is_valid_opu {
383        // Check for OLD invalid formats (old format didn't have i/o prefix)
384        if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
385        {
386            result.add_error(format!(
387                "INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
388                Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
389                Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
390                Valid IPU units: {:?}, Valid OPU units: {:?}. \
391                This genome needs migration to the new format.",
392                display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
393            ));
394        } else {
395            result.add_warning(format!(
396                "Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
397                Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
398                display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
399            ));
400        }
401        return;
402    }
403
404    // Validate the index/suffix part (characters 4-7, skipping i/o and unit prefix)
405    let suffix = &display[4..];
406
407    // For SegmentedVision (isvi), validate index (byte 4 should be 0-8)
408    if first_char == 'i' && unit_prefix == "svi" {
409        if let Some(index_char) = display.chars().nth(4) {
410            if index_char.is_ascii_digit() {
411                let digit = index_char as u8 - b'0';
412                if digit > 8 {
413                    result.add_error(format!(
414                        "Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
415                        digit, display
416                    ));
417                }
418            }
419        }
420    }
421
422    // Check that suffix is properly padded with underscores
423    if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
424        result.add_warning(format!(
425            "Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
426            display
427        ));
428    }
429}
430
431/// Validate morphologies
432fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
433    if genome.morphologies.count() == 0 {
434        result.add_warning("Genome has no morphologies defined".to_string());
435        return;
436    }
437
438    // Check for required core morphologies
439    let required_core = vec!["block_to_block", "projector"];
440    for morph_id in required_core {
441        if !genome.morphologies.contains(morph_id) {
442            result.add_warning(format!(
443                "Missing recommended core morphology: '{}'",
444                morph_id
445            ));
446        }
447    }
448
449    for (morphology_id, morphology) in genome.morphologies.iter() {
450        validate_single_morphology(morphology_id, morphology, result);
451    }
452}
453
454/// Validate a single morphology
455fn validate_single_morphology(
456    morphology_id: &str,
457    morphology: &crate::Morphology,
458    result: &mut ValidationResult,
459) {
460    match &morphology.parameters {
461        MorphologyParameters::Vectors { vectors } => {
462            if vectors.is_empty() {
463                result.add_error(format!(
464                    "Morphology '{}' (vectors) has no vectors defined",
465                    morphology_id
466                ));
467            }
468
469            // Check for all-zero vectors (useless)
470            for (i, vec) in vectors.iter().enumerate() {
471                if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
472                    result.add_warning(format!(
473                        "Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
474                        morphology_id, i, vec[0], vec[1], vec[2]
475                    ));
476                }
477            }
478        }
479
480        MorphologyParameters::Patterns { patterns } => {
481            if patterns.is_empty() {
482                result.add_error(format!(
483                    "Morphology '{}' (patterns) has no patterns defined",
484                    morphology_id
485                ));
486            }
487
488            for (i, pattern) in patterns.iter().enumerate() {
489                if pattern[0].len() != 3 || pattern[1].len() != 3 {
490                    result.add_error(format!(
491                        "Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
492                        morphology_id, i
493                    ));
494                }
495            }
496        }
497
498        MorphologyParameters::Functions {} => {
499            // Functions are built-in, no parameters to validate
500        }
501
502        MorphologyParameters::Composite {
503            src_seed,
504            src_pattern,
505            mapper_morphology,
506        } => {
507            // Validate src_seed
508            if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
509                result.add_warning(format!(
510                    "Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
511                    morphology_id, src_seed[0], src_seed[1], src_seed[2]
512                ));
513            }
514
515            // Validate src_pattern
516            if src_pattern.is_empty() {
517                result.add_error(format!(
518                    "Morphology '{}' (composite) has empty src_pattern",
519                    morphology_id
520                ));
521            }
522
523            // Validate mapper_morphology reference
524            if mapper_morphology.is_empty() {
525                result.add_error(format!(
526                    "Morphology '{}' (composite) has empty mapper_morphology reference",
527                    morphology_id
528                ));
529            }
530        }
531    }
532}
533
534/// Validate physiology parameters
535fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
536    let phys = &genome.physiology;
537
538    if phys.simulation_timestep <= 0.0 {
539        result.add_error(format!(
540            "Invalid simulation_timestep: {} (must be > 0.0)",
541            phys.simulation_timestep
542        ));
543    }
544
545    if phys.simulation_timestep > 1.0 {
546        result.add_warning(format!(
547            "Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
548            phys.simulation_timestep
549        ));
550    }
551
552    if phys.max_age == 0 {
553        result.add_warning("max_age is 0 (neurons will never age)".to_string());
554    }
555
556    if phys.plasticity_queue_depth == 0 {
557        result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
558    }
559
560    // Validate quantization_precision
561    validate_quantization_precision(&phys.quantization_precision, result);
562}
563
564/// Validate quantization precision value
565fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
566    use feagi_npu_neural::types::Precision;
567
568    // Try to parse the precision string
569    match Precision::from_str(precision) {
570        Ok(parsed_precision) => {
571            // Valid - log what was selected
572            if precision != parsed_precision.as_str() {
573                result.add_warning(format!(
574                    "Quantization precision '{}' normalized to '{}'",
575                    precision,
576                    parsed_precision.as_str()
577                ));
578            }
579        }
580        Err(_) => {
581            result.add_error(format!(
582                "Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
583                precision
584            ));
585        }
586    }
587}
588
589/// Cross-validate references between genome sections
590fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
591    // Build morphology ID set for quick lookup
592    let morphology_ids: HashSet<String> =
593        genome.morphologies.morphology_ids().into_iter().collect();
594
595    // Check if cortical areas reference morphologies in their properties
596    for (cortical_id, area) in &genome.cortical_areas {
597        let cortical_id_display = cortical_id.to_string();
598        if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
599            for (dest_area, rules) in dstmap {
600                // Check if destination area exists (convert string to CorticalID)
601                if let Ok(dest_cortical_id) =
602                    crate::genome::parser::string_to_cortical_id(dest_area)
603                {
604                    if !genome.cortical_areas.contains_key(&dest_cortical_id) {
605                        result.add_error(format!(
606                            "Cortical area '{}' references non-existent destination area '{}' in dstmap",
607                            cortical_id_display, dest_area
608                        ));
609                    }
610                } else {
611                    result.add_error(format!(
612                        "Cortical area '{}' has invalid destination area ID '{}' in dstmap",
613                        cortical_id_display, dest_area
614                    ));
615                }
616
617                // Check morphology references in rules
618                if let Value::Array(rules_array) = rules {
619                    for rule in rules_array {
620                        if let Value::Array(rule_array) = rule {
621                            if let Some(Value::String(morph_id)) = rule_array.first() {
622                                if !morphology_ids.contains(morph_id) {
623                                    result.add_error(format!(
624                                        "Cortical area '{}' references undefined morphology '{}' in dstmap rule",
625                                        cortical_id_display, morph_id
626                                    ));
627                                }
628                            }
629                        }
630                    }
631                }
632            }
633        }
634    }
635
636    // Validate brain region references
637    for (region_id, region) in &genome.brain_regions {
638        // Check if cortical areas in region exist
639        for cortical_id in &region.cortical_areas {
640            if !genome.cortical_areas.contains_key(cortical_id) {
641                result.add_error(format!(
642                    "Brain region '{}' references non-existent cortical area '{}'",
643                    region_id, cortical_id
644                ));
645            }
646        }
647    }
648
649    // Validate composite morphology references
650    for (morphology_id, morphology) in genome.morphologies.iter() {
651        if let MorphologyParameters::Composite {
652            mapper_morphology, ..
653        } = &morphology.parameters
654        {
655            if !morphology_ids.contains(mapper_morphology) {
656                result.add_error(format!(
657                    "Composite morphology '{}' references undefined mapper morphology '{}'",
658                    morphology_id, mapper_morphology
659                ));
660            }
661        }
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use crate::{
669        GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
670    };
671    use std::collections::HashMap;
672
673    #[test]
674    fn test_validate_empty_genome() {
675        let genome = RuntimeGenome {
676            metadata: GenomeMetadata {
677                genome_id: "test".to_string(),
678                genome_title: "Test".to_string(),
679                genome_description: "".to_string(),
680                version: "2.0".to_string(),
681                timestamp: 0.0,
682                brain_regions_root: None,
683            },
684            cortical_areas: HashMap::new(),
685            brain_regions: HashMap::new(),
686            morphologies: MorphologyRegistry::new(),
687            physiology: PhysiologyConfig::default(),
688            signatures: GenomeSignatures {
689                genome: "0".to_string(),
690                blueprint: "0".to_string(),
691                physiology: "0".to_string(),
692                morphologies: None,
693            },
694            stats: GenomeStats::default(),
695        };
696
697        let result = validate_genome(&genome);
698
699        // Should have warnings about empty cortical areas and morphologies
700        assert!(!result.warnings.is_empty());
701        println!("Warnings: {:?}", result.warnings);
702    }
703
704    #[test]
705    fn test_validate_valid_genome() {
706        let mut genome = RuntimeGenome {
707            metadata: GenomeMetadata {
708                genome_id: "test_genome".to_string(),
709                genome_title: "Test Genome".to_string(),
710                genome_description: "Valid test genome".to_string(),
711                version: "2.0".to_string(),
712                timestamp: 1234567890.0,
713                brain_regions_root: None,
714            },
715            cortical_areas: HashMap::new(),
716            brain_regions: HashMap::new(),
717            morphologies: MorphologyRegistry::new(),
718            physiology: PhysiologyConfig::default(),
719            signatures: GenomeSignatures {
720                genome: "abc123".to_string(),
721                blueprint: "def456".to_string(),
722                physiology: "ghi789".to_string(),
723                morphologies: None,
724            },
725            stats: GenomeStats::default(),
726        };
727
728        // Add a valid cortical area (use CoreCorticalType::Power)
729        use feagi_structures::genomic::cortical_area::CustomCorticalType;
730        use feagi_structures::genomic::cortical_area::{
731            CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
732        };
733        let test_id = CoreCorticalType::Power.to_cortical_id();
734        let area = CorticalArea::new(
735            test_id,
736            0,
737            "Test Area".to_string(),
738            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
739            (0, 0, 0).into(),
740            CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
741        )
742        .expect("Failed to create cortical area");
743
744        genome.cortical_areas.insert(test_id, area);
745
746        let result = validate_genome(&genome);
747
748        // Should pass with only warnings (empty morphologies)
749        println!("Errors: {:?}", result.errors);
750        println!("Warnings: {:?}", result.warnings);
751
752        // Genome is valid but has warnings
753        assert!(result.errors.is_empty());
754        assert!(!result.warnings.is_empty()); // Warning about no morphologies
755    }
756
757    #[test]
758    fn test_validate_quantization_precision() {
759        let mut genome = create_minimal_genome();
760
761        // Test 1: Valid precision (fp32)
762        genome.physiology.quantization_precision = "fp32".to_string();
763        let result = validate_genome(&genome);
764        assert!(result.errors.is_empty(), "fp32 should be valid");
765
766        // Test 2: Valid precision (int8)
767        genome.physiology.quantization_precision = "int8".to_string();
768        let result = validate_genome(&genome);
769        assert!(result.errors.is_empty(), "int8 should be valid");
770
771        // Test 3: Valid but non-canonical (i8 → int8)
772        genome.physiology.quantization_precision = "i8".to_string();
773        let result = validate_genome(&genome);
774        assert!(result.errors.is_empty(), "i8 should be valid");
775        assert!(
776            result.warnings.iter().any(|w| w.contains("normalized")),
777            "Should warn about normalization"
778        );
779
780        // Test 4: Invalid precision
781        genome.physiology.quantization_precision = "invalid".to_string();
782        let result = validate_genome(&genome);
783        assert!(!result.errors.is_empty(), "invalid should produce error");
784        assert!(
785            result
786                .errors
787                .iter()
788                .any(|e| e.contains("Invalid quantization_precision")),
789            "Should have quantization error"
790        );
791    }
792
793    #[test]
794    fn test_auto_fix_quantization_precision() {
795        // Test 1: Missing precision (empty string)
796        let mut genome = create_minimal_genome();
797        genome.physiology.quantization_precision = "".to_string();
798
799        let fixes = auto_fix_genome(&mut genome);
800        assert!(fixes > 0, "Should apply at least one fix");
801        assert_eq!(
802            genome.physiology.quantization_precision, "int8",
803            "Should default to int8"
804        );
805
806        // Test 2: Non-canonical (i8 → int8)
807        genome.physiology.quantization_precision = "i8".to_string();
808        let _fixes = auto_fix_genome(&mut genome);
809        assert_eq!(
810            genome.physiology.quantization_precision, "int8",
811            "Should normalize i8 to int8"
812        );
813
814        // Test 3: Invalid → default
815        genome.physiology.quantization_precision = "invalid".to_string();
816        let _fixes = auto_fix_genome(&mut genome);
817        assert_eq!(
818            genome.physiology.quantization_precision, "int8",
819            "Invalid should default to int8"
820        );
821    }
822
823    fn create_minimal_genome() -> RuntimeGenome {
824        RuntimeGenome {
825            metadata: GenomeMetadata {
826                genome_id: "test".to_string(),
827                genome_title: "Test".to_string(),
828                genome_description: "".to_string(),
829                version: "2.0".to_string(),
830                timestamp: 0.0,
831                brain_regions_root: None,
832            },
833            cortical_areas: HashMap::new(),
834            brain_regions: HashMap::new(),
835            morphologies: MorphologyRegistry::new(),
836            physiology: PhysiologyConfig::default(),
837            signatures: GenomeSignatures {
838                genome: "0".to_string(),
839                blueprint: "0".to_string(),
840                physiology: "0".to_string(),
841                morphologies: None,
842            },
843            stats: GenomeStats::default(),
844        }
845    }
846}