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