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        "dpt", // DepthMap
369    ];
370
371    // Known valid OPU prefixes from feagi-data-processing templates
372    const VALID_OPU_PREFIXES: &[&str] = &[
373        "mot", // Motor (omot____ variants)
374        "voc", // Vocal
375        "gaz", // Gaze control
376        "pse", // Positional Servo
377        "mis", // Miscellaneous
378    ];
379
380    let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
381    let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
382
383    if !is_valid_ipu && !is_valid_opu {
384        // Check for OLD invalid formats (old format didn't have i/o prefix)
385        if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
386        {
387            result.add_error(format!(
388                "INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
389                Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
390                Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
391                Valid IPU units: {:?}, Valid OPU units: {:?}. \
392                This genome needs migration to the new format.",
393                display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
394            ));
395        } else {
396            result.add_warning(format!(
397                "Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
398                Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
399                display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
400            ));
401        }
402        return;
403    }
404
405    // Validate the index/suffix part (characters 4-7, skipping i/o and unit prefix)
406    let suffix = &display[4..];
407
408    // For SegmentedVision (isvi), validate index (byte 4 should be 0-8)
409    if first_char == 'i' && unit_prefix == "svi" {
410        if let Some(index_char) = display.chars().nth(4) {
411            if index_char.is_ascii_digit() {
412                let digit = index_char as u8 - b'0';
413                if digit > 8 {
414                    result.add_error(format!(
415                        "Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
416                        digit, display
417                    ));
418                }
419            }
420        }
421    }
422
423    // Check that suffix is properly padded with underscores
424    if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
425        result.add_warning(format!(
426            "Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
427            display
428        ));
429    }
430}
431
432/// Validate morphologies
433fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
434    if genome.morphologies.count() == 0 {
435        result.add_warning("Genome has no morphologies defined".to_string());
436        return;
437    }
438
439    // Check for required core morphologies
440    let required_core = vec!["block_to_block", "projector"];
441    for morph_id in required_core {
442        if !genome.morphologies.contains(morph_id) {
443            result.add_warning(format!(
444                "Missing recommended core morphology: '{}'",
445                morph_id
446            ));
447        }
448    }
449
450    for (morphology_id, morphology) in genome.morphologies.iter() {
451        validate_single_morphology(morphology_id, morphology, result);
452    }
453}
454
455/// Validate a single morphology
456fn validate_single_morphology(
457    morphology_id: &str,
458    morphology: &crate::Morphology,
459    result: &mut ValidationResult,
460) {
461    match &morphology.parameters {
462        MorphologyParameters::Vectors { vectors } => {
463            if vectors.is_empty() {
464                result.add_error(format!(
465                    "Morphology '{}' (vectors) has no vectors defined",
466                    morphology_id
467                ));
468            }
469
470            // Check for all-zero vectors (useless)
471            for (i, vec) in vectors.iter().enumerate() {
472                if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
473                    result.add_warning(format!(
474                        "Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
475                        morphology_id, i, vec[0], vec[1], vec[2]
476                    ));
477                }
478            }
479        }
480
481        MorphologyParameters::Patterns { patterns } => {
482            if patterns.is_empty() {
483                result.add_error(format!(
484                    "Morphology '{}' (patterns) has no patterns defined",
485                    morphology_id
486                ));
487            }
488
489            for (i, pattern) in patterns.iter().enumerate() {
490                if pattern[0].len() != 3 || pattern[1].len() != 3 {
491                    result.add_error(format!(
492                        "Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
493                        morphology_id, i
494                    ));
495                }
496            }
497        }
498
499        MorphologyParameters::Functions {} => {
500            // Functions are built-in, no parameters to validate
501        }
502
503        MorphologyParameters::Composite {
504            src_seed,
505            src_pattern,
506            mapper_morphology,
507        } => {
508            // Validate src_seed
509            if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
510                result.add_warning(format!(
511                    "Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
512                    morphology_id, src_seed[0], src_seed[1], src_seed[2]
513                ));
514            }
515
516            // Validate src_pattern
517            if src_pattern.is_empty() {
518                result.add_error(format!(
519                    "Morphology '{}' (composite) has empty src_pattern",
520                    morphology_id
521                ));
522            }
523
524            // Validate mapper_morphology reference
525            if mapper_morphology.is_empty() {
526                result.add_error(format!(
527                    "Morphology '{}' (composite) has empty mapper_morphology reference",
528                    morphology_id
529                ));
530            }
531        }
532    }
533}
534
535/// Validate physiology parameters
536fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
537    let phys = &genome.physiology;
538
539    if phys.simulation_timestep <= 0.0 {
540        result.add_error(format!(
541            "Invalid simulation_timestep: {} (must be > 0.0)",
542            phys.simulation_timestep
543        ));
544    }
545
546    if phys.simulation_timestep > 1.0 {
547        result.add_warning(format!(
548            "Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
549            phys.simulation_timestep
550        ));
551    }
552
553    if phys.max_age == 0 {
554        result.add_warning("max_age is 0 (neurons will never age)".to_string());
555    }
556
557    if phys.plasticity_queue_depth == 0 {
558        result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
559    }
560
561    // Validate quantization_precision
562    validate_quantization_precision(&phys.quantization_precision, result);
563}
564
565/// Validate quantization precision value
566fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
567    use feagi_npu_neural::types::Precision;
568
569    // Try to parse the precision string
570    match Precision::from_str(precision) {
571        Ok(parsed_precision) => {
572            // Valid - log what was selected
573            if precision != parsed_precision.as_str() {
574                result.add_warning(format!(
575                    "Quantization precision '{}' normalized to '{}'",
576                    precision,
577                    parsed_precision.as_str()
578                ));
579            }
580        }
581        Err(_) => {
582            result.add_error(format!(
583                "Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
584                precision
585            ));
586        }
587    }
588}
589
590/// Cross-validate references between genome sections
591fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
592    // Build morphology ID set for quick lookup
593    let morphology_ids: HashSet<String> =
594        genome.morphologies.morphology_ids().into_iter().collect();
595
596    // Check if cortical areas reference morphologies in their properties
597    for (cortical_id, area) in &genome.cortical_areas {
598        let cortical_id_display = cortical_id.to_string();
599        if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
600            for (dest_area, rules) in dstmap {
601                // Check if destination area exists (convert string to CorticalID)
602                if let Ok(dest_cortical_id) =
603                    crate::genome::parser::string_to_cortical_id(dest_area)
604                {
605                    if !genome.cortical_areas.contains_key(&dest_cortical_id) {
606                        result.add_error(format!(
607                            "Cortical area '{}' references non-existent destination area '{}' in dstmap",
608                            cortical_id_display, dest_area
609                        ));
610                    }
611                } else {
612                    result.add_error(format!(
613                        "Cortical area '{}' has invalid destination area ID '{}' in dstmap",
614                        cortical_id_display, dest_area
615                    ));
616                }
617
618                // Check morphology references in rules
619                if let Value::Array(rules_array) = rules {
620                    for rule in rules_array {
621                        if let Value::Array(rule_array) = rule {
622                            if let Some(Value::String(morph_id)) = rule_array.first() {
623                                if !morphology_ids.contains(morph_id) {
624                                    result.add_error(format!(
625                                        "Cortical area '{}' references undefined morphology '{}' in dstmap rule",
626                                        cortical_id_display, morph_id
627                                    ));
628                                }
629                            }
630                        }
631                    }
632                }
633            }
634        }
635    }
636
637    // Validate brain region references
638    for (region_id, region) in &genome.brain_regions {
639        // Check if cortical areas in region exist
640        for cortical_id in &region.cortical_areas {
641            if !genome.cortical_areas.contains_key(cortical_id) {
642                result.add_error(format!(
643                    "Brain region '{}' references non-existent cortical area '{}'",
644                    region_id, cortical_id
645                ));
646            }
647        }
648    }
649
650    // Validate composite morphology references
651    for (morphology_id, morphology) in genome.morphologies.iter() {
652        if let MorphologyParameters::Composite {
653            mapper_morphology, ..
654        } = &morphology.parameters
655        {
656            if !morphology_ids.contains(mapper_morphology) {
657                result.add_error(format!(
658                    "Composite morphology '{}' references undefined mapper morphology '{}'",
659                    morphology_id, mapper_morphology
660                ));
661            }
662        }
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::{
670        GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
671    };
672    use std::collections::HashMap;
673
674    #[test]
675    fn test_validate_empty_genome() {
676        let genome = RuntimeGenome {
677            metadata: GenomeMetadata {
678                genome_id: "test".to_string(),
679                genome_title: "Test".to_string(),
680                genome_description: "".to_string(),
681                version: "2.0".to_string(),
682                timestamp: 0.0,
683                brain_regions_root: None,
684            },
685            cortical_areas: HashMap::new(),
686            brain_regions: HashMap::new(),
687            morphologies: MorphologyRegistry::new(),
688            physiology: PhysiologyConfig::default(),
689            signatures: GenomeSignatures {
690                genome: "0".to_string(),
691                blueprint: "0".to_string(),
692                physiology: "0".to_string(),
693                morphologies: None,
694            },
695            stats: GenomeStats::default(),
696        };
697
698        let result = validate_genome(&genome);
699
700        // Should have warnings about empty cortical areas and morphologies
701        assert!(!result.warnings.is_empty());
702        println!("Warnings: {:?}", result.warnings);
703    }
704
705    #[test]
706    fn test_validate_valid_genome() {
707        let mut genome = RuntimeGenome {
708            metadata: GenomeMetadata {
709                genome_id: "test_genome".to_string(),
710                genome_title: "Test Genome".to_string(),
711                genome_description: "Valid test genome".to_string(),
712                version: "2.0".to_string(),
713                timestamp: 1234567890.0,
714                brain_regions_root: None,
715            },
716            cortical_areas: HashMap::new(),
717            brain_regions: HashMap::new(),
718            morphologies: MorphologyRegistry::new(),
719            physiology: PhysiologyConfig::default(),
720            signatures: GenomeSignatures {
721                genome: "abc123".to_string(),
722                blueprint: "def456".to_string(),
723                physiology: "ghi789".to_string(),
724                morphologies: None,
725            },
726            stats: GenomeStats::default(),
727        };
728
729        // Add a valid cortical area (use CoreCorticalType::Power)
730        use feagi_structures::genomic::cortical_area::CustomCorticalType;
731        use feagi_structures::genomic::cortical_area::{
732            CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
733        };
734        let test_id = CoreCorticalType::Power.to_cortical_id();
735        let area = CorticalArea::new(
736            test_id,
737            0,
738            "Test Area".to_string(),
739            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
740            (0, 0, 0).into(),
741            CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
742        )
743        .expect("Failed to create cortical area");
744
745        genome.cortical_areas.insert(test_id, area);
746
747        let result = validate_genome(&genome);
748
749        // Should pass with only warnings (empty morphologies)
750        println!("Errors: {:?}", result.errors);
751        println!("Warnings: {:?}", result.warnings);
752
753        // Genome is valid but has warnings
754        assert!(result.errors.is_empty());
755        assert!(!result.warnings.is_empty()); // Warning about no morphologies
756    }
757
758    #[test]
759    fn test_validate_quantization_precision() {
760        let mut genome = create_minimal_genome();
761
762        // Test 1: Valid precision (fp32)
763        genome.physiology.quantization_precision = "fp32".to_string();
764        let result = validate_genome(&genome);
765        assert!(result.errors.is_empty(), "fp32 should be valid");
766
767        // Test 2: Valid precision (int8)
768        genome.physiology.quantization_precision = "int8".to_string();
769        let result = validate_genome(&genome);
770        assert!(result.errors.is_empty(), "int8 should be valid");
771
772        // Test 3: Valid but non-canonical (i8 → int8)
773        genome.physiology.quantization_precision = "i8".to_string();
774        let result = validate_genome(&genome);
775        assert!(result.errors.is_empty(), "i8 should be valid");
776        assert!(
777            result.warnings.iter().any(|w| w.contains("normalized")),
778            "Should warn about normalization"
779        );
780
781        // Test 4: Invalid precision
782        genome.physiology.quantization_precision = "invalid".to_string();
783        let result = validate_genome(&genome);
784        assert!(!result.errors.is_empty(), "invalid should produce error");
785        assert!(
786            result
787                .errors
788                .iter()
789                .any(|e| e.contains("Invalid quantization_precision")),
790            "Should have quantization error"
791        );
792    }
793
794    #[test]
795    fn test_auto_fix_quantization_precision() {
796        // Test 1: Missing precision (empty string)
797        let mut genome = create_minimal_genome();
798        genome.physiology.quantization_precision = "".to_string();
799
800        let fixes = auto_fix_genome(&mut genome);
801        assert!(fixes > 0, "Should apply at least one fix");
802        assert_eq!(
803            genome.physiology.quantization_precision, "int8",
804            "Should default to int8"
805        );
806
807        // Test 2: Non-canonical (i8 → int8)
808        genome.physiology.quantization_precision = "i8".to_string();
809        let _fixes = auto_fix_genome(&mut genome);
810        assert_eq!(
811            genome.physiology.quantization_precision, "int8",
812            "Should normalize i8 to int8"
813        );
814
815        // Test 3: Invalid → default
816        genome.physiology.quantization_precision = "invalid".to_string();
817        let _fixes = auto_fix_genome(&mut genome);
818        assert_eq!(
819            genome.physiology.quantization_precision, "int8",
820            "Invalid should default to int8"
821        );
822    }
823
824    fn create_minimal_genome() -> RuntimeGenome {
825        RuntimeGenome {
826            metadata: GenomeMetadata {
827                genome_id: "test".to_string(),
828                genome_title: "Test".to_string(),
829                genome_description: "".to_string(),
830                version: "2.0".to_string(),
831                timestamp: 0.0,
832                brain_regions_root: None,
833            },
834            cortical_areas: HashMap::new(),
835            brain_regions: HashMap::new(),
836            morphologies: MorphologyRegistry::new(),
837            physiology: PhysiologyConfig::default(),
838            signatures: GenomeSignatures {
839                genome: "0".to_string(),
840                blueprint: "0".to_string(),
841                physiology: "0".to_string(),
842                morphologies: None,
843            },
844            stats: GenomeStats::default(),
845        }
846    }
847}