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        validate_memory_mp_encoding(&area.properties, &cortical_id_display, result);
288    }
289}
290
291/// Validate MP encoding settings of a memory area.
292///
293/// Change modes need at least two frames; shallower windows are disabled at
294/// registration, so they are reported here as a warning rather than an error.
295fn validate_memory_mp_encoding(
296    properties: &std::collections::HashMap<String, Value>,
297    cortical_id_display: &str,
298    result: &mut ValidationResult,
299) {
300    let Some(mem_props) = crate::extract_memory_properties(properties) else {
301        return;
302    };
303    if let Err(e) = crate::validate_memory_mp_properties(properties) {
304        result.add_error(format!(
305            "Memory area '{}' has invalid MP encoding: {}",
306            cortical_id_display, e
307        ));
308        return;
309    }
310    if mem_props.mp_change_mode != crate::MpChangeMode::None && mem_props.temporal_depth < 2 {
311        result.add_warning(format!(
312            "Memory area '{}' uses {}='{}' with temporal_depth={}; change encoding needs temporal_depth >= 2 and will be disabled",
313            cortical_id_display,
314            crate::MP_CHANGE_MODE_KEY,
315            mem_props.mp_change_mode.as_str(),
316            mem_props.temporal_depth
317        ));
318    }
319}
320
321/// Validate cortical ID format and compliance with feagi-data-processing templates
322fn validate_cortical_id_format(
323    _cortical_id: &CorticalID,
324    display: &str,
325    result: &mut ValidationResult,
326) {
327    // Base64 encoded 8-byte IDs are 12 characters (with padding)
328    // Old format IDs are 8 characters
329    // Accept both formats for backward compatibility
330    if display.len() != 8 && display.len() != 12 {
331        result.add_error(format!(
332            "Invalid cortical ID length: '{}' is {} characters (must be 8 or 12)",
333            display,
334            display.len()
335        ));
336        return;
337    }
338
339    // Check if it's a CORE area (starts with underscore)
340    if display.starts_with('_') {
341        validate_core_area_id(display, result);
342        return;
343    }
344
345    // Check if it's a CUSTOM/MEMORY area (starts with 'c')
346    if display.starts_with('c') {
347        // Custom areas: No strict validation yet, but should follow naming conventions
348        // Just check that it's properly padded
349        if !display.chars().all(|c| c.is_alphanumeric() || c == '_') {
350            result.add_warning(format!(
351                "Custom cortical ID '{}' contains non-alphanumeric characters",
352                display
353            ));
354        }
355        return;
356    }
357
358    // Check if it's an IPU/OPU area (3-char prefix + 5 chars)
359    validate_io_area_id(display, result);
360}
361
362/// Validate CORE area IDs (power, death, etc.) using feagi-data-processing types
363fn validate_core_area_id(display: &str, result: &mut ValidationResult) {
364    use feagi_structures::genomic::cortical_area::CoreCorticalType;
365
366    // Generate valid CORE IDs from the authoritative source (feagi-data-processing)
367    let valid_core_ids: Vec<String> = vec![
368        CoreCorticalType::Power.to_cortical_id().to_string(), // "___power"
369        CoreCorticalType::Death.to_cortical_id().to_string(), // "___death"
370        CoreCorticalType::Fatigue.to_cortical_id().to_string(), // "___fatig"
371        CoreCorticalType::Pain.to_cortical_id().to_string(),  // "___pain_"
372        CoreCorticalType::Pleasure.to_cortical_id().to_string(), // "___pleas"
373        CoreCorticalType::Fear.to_cortical_id().to_string(),  // "___fear_"
374        CoreCorticalType::Hope.to_cortical_id().to_string(),  // "___hope_"
375    ];
376
377    if !valid_core_ids.contains(&display.to_string()) {
378        result.add_error(format!(
379            "Invalid CORE cortical ID: '{}' - must be one of: {:?}",
380            display, valid_core_ids
381        ));
382    }
383}
384
385/// Validate IPU/OPU area IDs (should follow template system)
386fn validate_io_area_id(display: &str, result: &mut ValidationResult) {
387    // IO cortical IDs have format: [i/o][3-char-unit][4-config-bytes]
388    // For IPU: 'i' + 3-char prefix (e.g., "isvi____")
389    // For OPU: 'o' + 3-char prefix (e.g., "omot____")
390    let first_char = display.chars().next().unwrap_or('_');
391    let unit_prefix = &display[1..4]; // Skip first char (i/o), get 3-char unit identifier
392
393    // Known valid IPU prefixes from feagi-data-processing templates
394    const VALID_IPU_PREFIXES: &[&str] = &[
395        "svi", // SegmentedVision (9 areas: isvi____ variants)
396        "aud", // Audio
397        "tac", // Tactile
398        "olf", // Olfactory
399        "vis", // Vision (generic)
400        "dpt", // DepthMap
401    ];
402
403    // Known valid OPU prefixes from feagi-data-processing templates
404    const VALID_OPU_PREFIXES: &[&str] = &[
405        "mot", // Motor (omot____ variants)
406        "voc", // Vocal
407        "gaz", // Gaze control
408        "pse", // Positional Servo
409        "mis", // Miscellaneous
410    ];
411
412    let is_valid_ipu = first_char == 'i' && VALID_IPU_PREFIXES.contains(&unit_prefix);
413    let is_valid_opu = first_char == 'o' && VALID_OPU_PREFIXES.contains(&unit_prefix);
414
415    if !is_valid_ipu && !is_valid_opu {
416        // Check for OLD invalid formats (old format didn't have i/o prefix)
417        if display.starts_with("iic") || display.starts_with("omot") || display.starts_with("ogaz")
418        {
419            result.add_error(format!(
420                "INVALID OLD-FORMAT cortical ID: '{}' - not compliant with feagi-data-processing templates. \
421                Valid IPU format: 'i' + unit_prefix (e.g., 'isvi____'). \
422                Valid OPU format: 'o' + unit_prefix (e.g., 'omot____'). \
423                Valid IPU units: {:?}, Valid OPU units: {:?}. \
424                This genome needs migration to the new format.",
425                display, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
426            ));
427        } else {
428            result.add_warning(format!(
429                "Unknown cortical ID: '{}' (first char: '{}', unit: '{}') - may not follow feagi-data-processing template system. \
430                Valid IPU format: 'i' + {:?}. Valid OPU format: 'o' + {:?}",
431                display, first_char, unit_prefix, VALID_IPU_PREFIXES, VALID_OPU_PREFIXES
432            ));
433        }
434        return;
435    }
436
437    // Validate the index/suffix part (characters 4-7, skipping i/o and unit prefix)
438    let suffix = &display[4..];
439
440    // For SegmentedVision (isvi), validate index (byte 4 should be 0-8)
441    if first_char == 'i' && unit_prefix == "svi" {
442        if let Some(index_char) = display.chars().nth(4) {
443            if index_char.is_ascii_digit() {
444                let digit = index_char as u8 - b'0';
445                if digit > 8 {
446                    result.add_error(format!(
447                        "Invalid SegmentedVision index: '{}' in '{}' - SegmentedVision has 9 areas (indices 0-8)",
448                        digit, display
449                    ));
450                }
451            }
452        }
453    }
454
455    // Check that suffix is properly padded with underscores
456    if !suffix.chars().all(|c| c.is_alphanumeric() || c == '_') {
457        result.add_warning(format!(
458            "Cortical ID '{}' has invalid characters in suffix (should be alphanumeric or underscore)",
459            display
460        ));
461    }
462}
463
464/// Validate morphologies
465fn validate_morphologies(genome: &RuntimeGenome, result: &mut ValidationResult) {
466    if genome.morphologies.count() == 0 {
467        result.add_warning("Genome has no morphologies defined".to_string());
468        return;
469    }
470
471    // Check for required core morphologies
472    let required_core = vec!["block_to_block", "projector"];
473    for morph_id in required_core {
474        if !genome.morphologies.contains(morph_id) {
475            result.add_warning(format!(
476                "Missing recommended core morphology: '{}'",
477                morph_id
478            ));
479        }
480    }
481
482    for (morphology_id, morphology) in genome.morphologies.iter() {
483        validate_single_morphology(morphology_id, morphology, result);
484    }
485}
486
487/// Validate a single morphology
488fn validate_single_morphology(
489    morphology_id: &str,
490    morphology: &crate::Morphology,
491    result: &mut ValidationResult,
492) {
493    match &morphology.parameters {
494        MorphologyParameters::Vectors { vectors } => {
495            if vectors.is_empty() {
496                result.add_error(format!(
497                    "Morphology '{}' (vectors) has no vectors defined",
498                    morphology_id
499                ));
500            }
501
502            // Check for all-zero vectors (useless)
503            for (i, vec) in vectors.iter().enumerate() {
504                if vec[0] == 0 && vec[1] == 0 && vec[2] == 0 {
505                    result.add_warning(format!(
506                        "Morphology '{}' has zero vector at index {}: [{}, {}, {}]",
507                        morphology_id, i, vec[0], vec[1], vec[2]
508                    ));
509                }
510            }
511        }
512
513        MorphologyParameters::Patterns { patterns } => {
514            if patterns.is_empty() {
515                result.add_error(format!(
516                    "Morphology '{}' (patterns) has no patterns defined",
517                    morphology_id
518                ));
519            }
520
521            for (i, pattern) in patterns.iter().enumerate() {
522                if pattern[0].len() != 3 || pattern[1].len() != 3 {
523                    result.add_error(format!(
524                        "Morphology '{}' pattern {} has invalid structure (expected [src[3], dst[3]])",
525                        morphology_id, i
526                    ));
527                }
528            }
529        }
530
531        MorphologyParameters::Functions {} => {
532            // Functions are built-in, no parameters to validate
533        }
534
535        MorphologyParameters::Composite {
536            src_seed,
537            src_pattern,
538            mapper_morphology,
539        } => {
540            // Validate src_seed
541            if src_seed[0] == 0 || src_seed[1] == 0 || src_seed[2] == 0 {
542                result.add_warning(format!(
543                    "Morphology '{}' has zero dimension in src_seed: [{}, {}, {}]",
544                    morphology_id, src_seed[0], src_seed[1], src_seed[2]
545                ));
546            }
547
548            // Validate src_pattern
549            if src_pattern.is_empty() {
550                result.add_error(format!(
551                    "Morphology '{}' (composite) has empty src_pattern",
552                    morphology_id
553                ));
554            }
555
556            // Validate mapper_morphology reference
557            if mapper_morphology.is_empty() {
558                result.add_error(format!(
559                    "Morphology '{}' (composite) has empty mapper_morphology reference",
560                    morphology_id
561                ));
562            }
563        }
564    }
565}
566
567/// Validate physiology parameters
568fn validate_physiology(genome: &RuntimeGenome, result: &mut ValidationResult) {
569    let phys = &genome.physiology;
570
571    if phys.simulation_timestep <= 0.0 {
572        result.add_error(format!(
573            "Invalid simulation_timestep: {} (must be > 0.0)",
574            phys.simulation_timestep
575        ));
576    }
577
578    if phys.simulation_timestep > 1.0 {
579        result.add_warning(format!(
580            "Very large simulation_timestep: {} seconds (typical: 0.01-0.1)",
581            phys.simulation_timestep
582        ));
583    }
584
585    if phys.max_age == 0 {
586        result.add_warning("max_age is 0 (neurons will never age)".to_string());
587    }
588
589    if phys.plasticity_queue_depth == 0 {
590        result.add_warning("plasticity_queue_depth is 0 (no plasticity history)".to_string());
591    }
592
593    // Validate quantization_precision
594    validate_quantization_precision(&phys.quantization_precision, result);
595}
596
597/// Validate quantization precision value
598fn validate_quantization_precision(precision: &str, result: &mut ValidationResult) {
599    use feagi_npu_neural::types::Precision;
600
601    // Try to parse the precision string
602    match Precision::from_str(precision) {
603        Ok(parsed_precision) => {
604            // Valid - log what was selected
605            if precision != parsed_precision.as_str() {
606                result.add_warning(format!(
607                    "Quantization precision '{}' normalized to '{}'",
608                    precision,
609                    parsed_precision.as_str()
610                ));
611            }
612        }
613        Err(_) => {
614            result.add_error(format!(
615                "Invalid quantization_precision: '{}' (must be 'fp32', 'fp16', or 'int8')",
616                precision
617            ));
618        }
619    }
620}
621
622/// Cross-validate references between genome sections
623fn cross_validate(genome: &RuntimeGenome, result: &mut ValidationResult) {
624    // Build morphology ID set for quick lookup
625    let morphology_ids: HashSet<String> =
626        genome.morphologies.morphology_ids().into_iter().collect();
627
628    // Check if cortical areas reference morphologies in their properties
629    for (cortical_id, area) in &genome.cortical_areas {
630        let cortical_id_display = cortical_id.to_string();
631        if let Some(Value::Object(dstmap)) = area.properties.get("dstmap") {
632            for (dest_area, rules) in dstmap {
633                // Check if destination area exists (convert string to CorticalID)
634                if let Ok(dest_cortical_id) =
635                    crate::genome::parser::string_to_cortical_id(dest_area)
636                {
637                    if !genome.cortical_areas.contains_key(&dest_cortical_id) {
638                        result.add_error(format!(
639                            "Cortical area '{}' references non-existent destination area '{}' in dstmap",
640                            cortical_id_display, dest_area
641                        ));
642                    }
643                } else {
644                    result.add_error(format!(
645                        "Cortical area '{}' has invalid destination area ID '{}' in dstmap",
646                        cortical_id_display, dest_area
647                    ));
648                }
649
650                // Check morphology references in rules
651                if let Value::Array(rules_array) = rules {
652                    for rule in rules_array {
653                        if let Value::Array(rule_array) = rule {
654                            if let Some(Value::String(morph_id)) = rule_array.first() {
655                                if !morphology_ids.contains(morph_id) {
656                                    result.add_error(format!(
657                                        "Cortical area '{}' references undefined morphology '{}' in dstmap rule",
658                                        cortical_id_display, morph_id
659                                    ));
660                                }
661                            }
662                        }
663                    }
664                }
665            }
666        }
667    }
668
669    // Validate brain region references
670    for (region_id, region) in &genome.brain_regions {
671        // Check if cortical areas in region exist
672        for cortical_id in &region.cortical_areas {
673            if !genome.cortical_areas.contains_key(cortical_id) {
674                result.add_error(format!(
675                    "Brain region '{}' references non-existent cortical area '{}'",
676                    region_id, cortical_id
677                ));
678            }
679        }
680    }
681
682    for (classifier_id, classifier) in &genome.classifiers {
683        if classifier.name.trim().is_empty() {
684            result.add_error(format!("Classifier '{}' has an empty name", classifier_id));
685        }
686        if !genome
687            .brain_regions
688            .contains_key(&classifier.parent_region_id)
689        {
690            result.add_error(format!(
691                "Classifier '{}' references unknown parent_region_id '{}'",
692                classifier_id, classifier.parent_region_id
693            ));
694        }
695        for area_id in classifier.owned_area_ids() {
696            if crate::genome::parser::string_to_cortical_id(&area_id)
697                .ok()
698                .and_then(|id| genome.cortical_areas.get(&id).map(|_| ()))
699                .is_none()
700            {
701                result.add_error(format!(
702                    "Classifier '{}' references missing owned area '{}'",
703                    classifier_id, area_id
704                ));
705            }
706        }
707        for area_id in classifier.input_area_ids() {
708            if crate::genome::parser::string_to_cortical_id(&area_id)
709                .ok()
710                .and_then(|id| genome.cortical_areas.get(&id).map(|_| ()))
711                .is_none()
712            {
713                result.add_error(format!(
714                    "Classifier '{}' references missing input area '{}'",
715                    classifier_id, area_id
716                ));
717            }
718        }
719    }
720
721    // Validate composite morphology references
722    for (morphology_id, morphology) in genome.morphologies.iter() {
723        if let MorphologyParameters::Composite {
724            mapper_morphology, ..
725        } = &morphology.parameters
726        {
727            if !morphology_ids.contains(mapper_morphology) {
728                result.add_error(format!(
729                    "Composite morphology '{}' references undefined mapper morphology '{}'",
730                    morphology_id, mapper_morphology
731                ));
732            }
733        }
734    }
735}
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740    use crate::{
741        GenomeMetadata, GenomeSignatures, GenomeStats, MorphologyRegistry, PhysiologyConfig,
742    };
743    use std::collections::HashMap;
744
745    #[test]
746    fn test_validate_empty_genome() {
747        let genome = RuntimeGenome {
748            metadata: GenomeMetadata {
749                genome_id: "test".to_string(),
750                genome_title: "Test".to_string(),
751                genome_description: "".to_string(),
752                version: "2.0".to_string(),
753                timestamp: 0.0,
754                brain_regions_root: None,
755            },
756            cortical_areas: HashMap::new(),
757            brain_regions: HashMap::new(),
758            classifiers: HashMap::new(),
759            morphologies: MorphologyRegistry::new(),
760            physiology: PhysiologyConfig::default(),
761            signatures: GenomeSignatures {
762                genome: "0".to_string(),
763                blueprint: "0".to_string(),
764                physiology: "0".to_string(),
765                morphologies: None,
766            },
767            stats: GenomeStats::default(),
768        };
769
770        let result = validate_genome(&genome);
771
772        // Should have warnings about empty cortical areas and morphologies
773        assert!(!result.warnings.is_empty());
774        println!("Warnings: {:?}", result.warnings);
775    }
776
777    #[test]
778    fn test_validate_valid_genome() {
779        let mut genome = RuntimeGenome {
780            metadata: GenomeMetadata {
781                genome_id: "test_genome".to_string(),
782                genome_title: "Test Genome".to_string(),
783                genome_description: "Valid test genome".to_string(),
784                version: "2.0".to_string(),
785                timestamp: 1234567890.0,
786                brain_regions_root: None,
787            },
788            cortical_areas: HashMap::new(),
789            brain_regions: HashMap::new(),
790            classifiers: HashMap::new(),
791            morphologies: MorphologyRegistry::new(),
792            physiology: PhysiologyConfig::default(),
793            signatures: GenomeSignatures {
794                genome: "abc123".to_string(),
795                blueprint: "def456".to_string(),
796                physiology: "ghi789".to_string(),
797                morphologies: None,
798            },
799            stats: GenomeStats::default(),
800        };
801
802        // Add a valid cortical area (use CoreCorticalType::Power)
803        use feagi_structures::genomic::cortical_area::CustomCorticalType;
804        use feagi_structures::genomic::cortical_area::{
805            CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
806        };
807        let test_id = CoreCorticalType::Power.to_cortical_id();
808        let area = CorticalArea::new(
809            test_id,
810            0,
811            "Test Area".to_string(),
812            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
813            (0, 0, 0).into(),
814            CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire),
815        )
816        .expect("Failed to create cortical area");
817
818        genome.cortical_areas.insert(test_id, area);
819
820        let result = validate_genome(&genome);
821
822        // Should pass with only warnings (empty morphologies)
823        println!("Errors: {:?}", result.errors);
824        println!("Warnings: {:?}", result.warnings);
825
826        // Genome is valid but has warnings
827        assert!(result.errors.is_empty());
828        assert!(!result.warnings.is_empty()); // Warning about no morphologies
829    }
830
831    #[test]
832    fn test_validate_quantization_precision() {
833        let mut genome = create_minimal_genome();
834
835        // Test 1: Valid precision (fp32)
836        genome.physiology.quantization_precision = "fp32".to_string();
837        let result = validate_genome(&genome);
838        assert!(result.errors.is_empty(), "fp32 should be valid");
839
840        // Test 2: Valid precision (int8)
841        genome.physiology.quantization_precision = "int8".to_string();
842        let result = validate_genome(&genome);
843        assert!(result.errors.is_empty(), "int8 should be valid");
844
845        // Test 3: Valid but non-canonical (i8 → int8)
846        genome.physiology.quantization_precision = "i8".to_string();
847        let result = validate_genome(&genome);
848        assert!(result.errors.is_empty(), "i8 should be valid");
849        assert!(
850            result.warnings.iter().any(|w| w.contains("normalized")),
851            "Should warn about normalization"
852        );
853
854        // Test 4: Invalid precision
855        genome.physiology.quantization_precision = "invalid".to_string();
856        let result = validate_genome(&genome);
857        assert!(!result.errors.is_empty(), "invalid should produce error");
858        assert!(
859            result
860                .errors
861                .iter()
862                .any(|e| e.contains("Invalid quantization_precision")),
863            "Should have quantization error"
864        );
865    }
866
867    #[test]
868    fn test_auto_fix_quantization_precision() {
869        // Test 1: Missing precision (empty string)
870        let mut genome = create_minimal_genome();
871        genome.physiology.quantization_precision = "".to_string();
872
873        let fixes = auto_fix_genome(&mut genome);
874        assert!(fixes > 0, "Should apply at least one fix");
875        assert_eq!(
876            genome.physiology.quantization_precision, "int8",
877            "Should default to int8"
878        );
879
880        // Test 2: Non-canonical (i8 → int8)
881        genome.physiology.quantization_precision = "i8".to_string();
882        let _fixes = auto_fix_genome(&mut genome);
883        assert_eq!(
884            genome.physiology.quantization_precision, "int8",
885            "Should normalize i8 to int8"
886        );
887
888        // Test 3: Invalid → default
889        genome.physiology.quantization_precision = "invalid".to_string();
890        let _fixes = auto_fix_genome(&mut genome);
891        assert_eq!(
892            genome.physiology.quantization_precision, "int8",
893            "Invalid should default to int8"
894        );
895    }
896
897    #[test]
898    fn test_validate_classifier_requires_parent_region() {
899        let mut genome = create_minimal_genome();
900        genome.classifiers.insert(
901            "clf-1".to_string(),
902            feagi_structures::genomic::classifiers::Classifier {
903                classifier_id: "clf-1".to_string(),
904                name: "demo".to_string(),
905                parent_region_id: "missing".to_string(),
906                coordinates_3d: [0, 0, 0],
907                training_mode:
908                    feagi_structures::genomic::classifiers::ClassifierTrainingMode::Kernel,
909                kernel_area_id: None,
910                class_area_id: None,
911                mask_area_id: None,
912                class_count: None,
913                kernel_size: None,
914                fields: vec![feagi_structures::genomic::classifiers::ClassifierField {
915                    field_area_id: "cfield".to_string(),
916                    scan_twin_id: "cscan1".to_string(),
917                }],
918                kernel_memory_id: "mkmem1".to_string(),
919                class_memory_id: "mcmem1".to_string(),
920                reward_training: false,
921                answer_feedback_area_id: None,
922                pain_area_id: None,
923                pleasure_area_id: None,
924                answer_latency_bursts: 0,
925                learn_area_id: None,
926                confidence_area_id: None,
927                properties: HashMap::new(),
928            },
929        );
930        let result = validate_genome(&genome);
931        assert!(result
932            .errors
933            .iter()
934            .any(|error| error.contains("unknown parent_region_id")));
935        assert!(result
936            .errors
937            .iter()
938            .any(|error| error.contains("missing owned area")));
939    }
940
941    fn create_minimal_genome() -> RuntimeGenome {
942        RuntimeGenome {
943            metadata: GenomeMetadata {
944                genome_id: "test".to_string(),
945                genome_title: "Test".to_string(),
946                genome_description: "".to_string(),
947                version: "2.0".to_string(),
948                timestamp: 0.0,
949                brain_regions_root: None,
950            },
951            cortical_areas: HashMap::new(),
952            brain_regions: HashMap::new(),
953            classifiers: HashMap::new(),
954            morphologies: MorphologyRegistry::new(),
955            physiology: PhysiologyConfig::default(),
956            signatures: GenomeSignatures {
957                genome: "0".to_string(),
958                blueprint: "0".to_string(),
959                physiology: "0".to_string(),
960                morphologies: None,
961            },
962            stats: GenomeStats::default(),
963        }
964    }
965}