Skip to main content

feagi_evolutionary/genome/
parser.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Genome JSON parser.
6
7Parses FEAGI 2.1 genome JSON format into runtime data structures.
8
9## Genome Structure (v2.1)
10
11```json
12{
13  "genome_id": "...",
14  "genome_title": "...",
15  "version": "2.1",
16  "blueprint": {
17    "cortical_id": {
18      "cortical_name": "...",
19      "block_boundaries": [x, y, z],
20      "relative_coordinate": [x, y, z],
21      "cortical_type": "IPU/OPU/CUSTOM/CORE/MEMORY",
22      ...
23    }
24  },
25  "brain_regions": {
26    "root": {
27      "title": "...",
28      "parent_region_id": null,
29      "coordinate_3d": [x, y, z],
30      "areas": ["cortical_id1", ...],
31      "regions": ["child_region_id1", ...]
32    }
33  },
34  "neuron_morphologies": { ... },
35  "physiology": { ... }
36}
37```
38
39Copyright 2025 Neuraville Inc.
40Licensed under the Apache License, Version 2.0
41*/
42
43use serde::{Deserialize, Serialize};
44use serde_json::Value;
45use std::collections::HashMap;
46use tracing::warn;
47
48use crate::types::{EvoError, EvoResult};
49use feagi_structures::genomic::brain_regions::RegionID;
50use feagi_structures::genomic::classifiers::Classifier;
51use feagi_structures::genomic::cortical_area::CorticalID;
52use feagi_structures::genomic::cortical_area::{
53    CorticalArea, CorticalAreaDimensions as Dimensions,
54};
55use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
56use feagi_structures::genomic::{BrainRegion, RegionType};
57
58/// Parsed genome data ready for ConnectomeManager
59#[derive(Debug, Clone)]
60pub struct ParsedGenome {
61    /// Genome metadata
62    pub genome_id: String,
63    pub genome_title: String,
64    pub version: String,
65
66    /// Cortical areas extracted from blueprint
67    pub cortical_areas: Vec<CorticalArea>,
68
69    /// Brain regions and hierarchy
70    pub brain_regions: Vec<(BrainRegion, Option<String>)>, // (region, parent_id)
71
72    /// First-class classifier assemblies (parallel to brain_regions)
73    pub classifiers: Vec<Classifier>,
74
75    /// Raw neuron morphologies (for later processing)
76    pub neuron_morphologies: HashMap<String, Value>,
77
78    /// Raw physiology data (for later processing)
79    pub physiology: Option<Value>,
80
81    /// Recorded structural changes, carried as-is (see `RuntimeGenome::change_history`)
82    pub change_history: Vec<Value>,
83}
84
85/// Raw genome JSON structure for deserialization
86#[derive(Debug, Clone, Deserialize, Serialize)]
87pub struct RawGenome {
88    pub genome_id: Option<String>,
89    pub genome_title: Option<String>,
90    pub genome_description: Option<String>,
91    pub version: String,
92    /// Integer schema version. Optional on the wire so older genomes that
93    /// pre-date this field still deserialize. The authoritative resolver
94    /// is `crate::genome::schema::detect_schema_version` and consumers
95    /// MUST go through it instead of branching on this field directly.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub genome_schema_version: Option<u32>,
98    pub blueprint: HashMap<String, RawCorticalArea>,
99    #[serde(default)]
100    pub brain_regions: HashMap<String, RawBrainRegion>,
101    #[serde(default)]
102    pub classifiers: HashMap<String, RawClassifier>,
103    #[serde(default)]
104    pub neuron_morphologies: HashMap<String, Value>,
105    #[serde(default)]
106    pub physiology: Option<Value>,
107    /// Root brain region ID (UUID string) - for O(1) root lookup
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub brain_regions_root: Option<String>,
110    /// Recorded structural changes (optional; see `RuntimeGenome::change_history`)
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    pub change_history: Vec<Value>,
113}
114
115/// Raw cortical area from blueprint
116#[derive(Debug, Clone, Deserialize, Serialize)]
117pub struct RawCorticalArea {
118    pub cortical_name: Option<String>,
119    pub block_boundaries: Option<Vec<u32>>,
120    pub relative_coordinate: Option<Vec<i32>>,
121    pub cortical_type: Option<String>,
122
123    // Optional properties
124    pub group_id: Option<String>,
125    pub sub_group_id: Option<String>,
126    pub per_voxel_neuron_cnt: Option<u32>,
127    pub cortical_mapping_dst: Option<Value>,
128
129    // Neural properties
130    pub synapse_attractivity: Option<f32>,
131    pub refractory_period: Option<u32>,
132    pub firing_threshold: Option<f32>,
133    pub firing_threshold_limit: Option<f32>,
134    pub firing_threshold_increment_x: Option<f32>,
135    pub firing_threshold_increment_y: Option<f32>,
136    pub firing_threshold_increment_z: Option<f32>,
137    pub leak_coefficient: Option<f32>,
138    pub leak_variability: Option<f32>,
139    pub neuron_excitability: Option<f32>,
140    pub postsynaptic_current: Option<f32>,
141    pub postsynaptic_current_max: Option<f32>,
142    pub degeneration: Option<f32>,
143    pub psp_uniform_distribution: Option<bool>,
144    pub mp_charge_accumulation: Option<bool>,
145    pub mp_driven_psp: Option<bool>,
146    pub visualization: Option<bool>,
147    pub burst_engine_activation: Option<bool>,
148    #[serde(rename = "2d_coordinate")]
149    pub coordinate_2d: Option<Vec<i32>>,
150
151    // Memory properties
152    pub is_mem_type: Option<bool>,
153    pub longterm_mem_threshold: Option<u32>,
154    pub lifespan_growth_rate: Option<f32>,
155    pub init_lifespan: Option<u32>,
156    pub temporal_depth: Option<u32>,
157    pub mp_learning_enabled: Option<bool>,
158    pub mp_change_mode: Option<String>,
159    pub mp_delta_quantization: Option<f32>,
160    pub mp_ratio_quantization: Option<f32>,
161    pub min_window_activity: Option<u32>,
162    pub scan_skip_density: Option<f32>,
163    pub consecutive_fire_cnt_max: Option<u32>,
164    pub snooze_length: Option<u32>,
165
166    // Allow any other properties (future-proofing)
167    #[serde(flatten)]
168    pub other: HashMap<String, Value>,
169}
170
171/// Raw brain region from genome
172#[derive(Debug, Clone, Deserialize, Serialize)]
173pub struct RawBrainRegion {
174    #[serde(alias = "name")]
175    pub title: Option<String>,
176    pub description: Option<String>,
177    pub parent_region_id: Option<String>,
178    pub coordinate_2d: Option<Vec<i32>>,
179    pub coordinate_3d: Option<Vec<i32>>,
180    #[serde(alias = "cortical_areas")]
181    pub areas: Option<Vec<String>>,
182    pub regions: Option<Vec<String>>,
183    pub inputs: Option<Vec<String>>,
184    pub outputs: Option<Vec<String>>,
185    /// Declared interface lists (persisted from RuntimeGenome / PUT region).
186    pub designated_inputs: Option<Vec<String>>,
187    pub designated_outputs: Option<Vec<String>>,
188    pub signature: Option<String>,
189    /// v3 `serde_json::to_value(BrainRegion)` nests `inputs` / `designated_*` under `properties`.
190    pub properties: Option<HashMap<String, Value>>,
191}
192
193/// Raw classifier assembly from the top-level `classifiers` genome key.
194#[derive(Debug, Clone, Deserialize, Serialize)]
195pub struct RawClassifier {
196    #[serde(alias = "title")]
197    pub name: Option<String>,
198    pub parent_region_id: Option<String>,
199    #[serde(alias = "coordinate_3d")]
200    pub coordinates_3d: Option<Vec<i32>>,
201    pub kernel_area_id: Option<String>,
202    pub class_area_id: Option<String>,
203    #[serde(default)]
204    pub training_mode: Option<feagi_structures::genomic::classifiers::ClassifierTrainingMode>,
205    pub mask_area_id: Option<String>,
206    pub kernel_size: Option<[u32; 3]>,
207    /// Scanner-mode class count. Required in scanner mode.
208    pub class_count: Option<u32>,
209    /// Current field bindings. Each entry is one Classifier mapping and its twin.
210    pub fields: Option<Vec<feagi_structures::genomic::classifiers::ClassifierField>>,
211    /// Previous singular field record. Loaded as one binding when `fields` is absent.
212    pub field_area_id: Option<String>,
213    pub kernel_memory_id: Option<String>,
214    pub class_memory_id: Option<String>,
215    #[serde(default)]
216    pub reward_training: bool,
217    #[serde(default)]
218    pub answer_feedback_area_id: Option<String>,
219    #[serde(default)]
220    pub pain_area_id: Option<String>,
221    #[serde(default)]
222    pub pleasure_area_id: Option<String>,
223    #[serde(default)]
224    pub answer_latency_bursts: u32,
225    #[serde(default)]
226    pub learn_area_id: Option<String>,
227    #[serde(default)]
228    pub confidence_area_id: Option<String>,
229    /// Previous singular twin record. Paired with `field_area_id` on load.
230    pub scan_twin_id: Option<String>,
231    pub properties: Option<HashMap<String, Value>>,
232}
233
234fn classifier_fields_from_raw(
235    raw: &RawClassifier,
236) -> Vec<feagi_structures::genomic::classifiers::ClassifierField> {
237    if let Some(fields) = &raw.fields {
238        return fields
239            .iter()
240            .filter(|field| !field.field_area_id.is_empty() && !field.scan_twin_id.is_empty())
241            .cloned()
242            .collect();
243    }
244    match (&raw.field_area_id, &raw.scan_twin_id) {
245        (Some(field_area_id), Some(scan_twin_id))
246            if !field_area_id.is_empty() && !scan_twin_id.is_empty() =>
247        {
248            vec![feagi_structures::genomic::classifiers::ClassifierField {
249                field_area_id: field_area_id.clone(),
250                scan_twin_id: scan_twin_id.clone(),
251            }]
252        }
253        _ => Vec::new(),
254    }
255}
256
257/// Convert cortical_mapping_dst keys from old format to base64
258///
259/// This ensures all destination cortical IDs in dstmap are stored in the new base64 format.
260fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
261    if let Some(dstmap_obj) = dstmap.as_object() {
262        let mut converted = serde_json::Map::new();
263
264        for (dest_id_str, mapping_value) in dstmap_obj {
265            // Convert destination cortical_id to base64 format
266            match string_to_cortical_id(dest_id_str) {
267                Ok(dest_cortical_id) => {
268                    converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
269                }
270                Err(e) => {
271                    // If conversion fails, keep original and log warning
272                    tracing::warn!(
273                        "Failed to convert dstmap key '{}' to base64: {}, keeping original",
274                        dest_id_str,
275                        e
276                    );
277                    converted.insert(dest_id_str.clone(), mapping_value.clone());
278                }
279            }
280        }
281
282        Value::Object(converted)
283    } else {
284        // Not an object, return as-is
285        dstmap.clone()
286    }
287}
288
289/// Convert a string cortical_id to CorticalID
290/// Handles both old 6-char format and new base64 format
291/// CRITICAL: Uses feagi-data-processing types as single source of truth for core areas
292pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
293    use feagi_structures::genomic::cortical_area::CoreCorticalType;
294
295    // Try base64 first (new format)
296    if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
297        let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
298        cortical_id.write_id_to_bytes(&mut bytes);
299        if bytes == *b"___power" {
300            return Ok(CoreCorticalType::Power.to_cortical_id());
301        }
302        if bytes == *b"___death" {
303            return Ok(CoreCorticalType::Death.to_cortical_id());
304        }
305        if bytes == *b"___fatig" {
306            return Ok(CoreCorticalType::Fatigue.to_cortical_id());
307        }
308        if bytes == *b"___pain_" {
309            return Ok(CoreCorticalType::Pain.to_cortical_id());
310        }
311        if bytes == *b"___pleas" {
312            return Ok(CoreCorticalType::Pleasure.to_cortical_id());
313        }
314        if bytes == *b"___fear_" {
315            return Ok(CoreCorticalType::Fear.to_cortical_id());
316        }
317        if bytes == *b"___hope_" {
318            return Ok(CoreCorticalType::Hope.to_cortical_id());
319        }
320        return Ok(cortical_id);
321    }
322
323    // Handle legacy CORE area names (6-char format) - use proper types from feagi-data-processing
324    if id_str == "_power" {
325        return Ok(CoreCorticalType::Power.to_cortical_id());
326    }
327    // Legacy shorthand used by older FEAGI genomes: "___pwr" (6-char) refers to core Power.
328    if id_str == "___pwr" {
329        return Ok(CoreCorticalType::Power.to_cortical_id());
330    }
331    // Legacy 8-char core names used in some BV caches
332    if id_str == "___power" {
333        return Ok(CoreCorticalType::Power.to_cortical_id());
334    }
335    // 8-char padded form of ___pwr (from 6-char padding in legacy flat genomes)
336    if id_str == "___pwr__" {
337        return Ok(CoreCorticalType::Power.to_cortical_id());
338    }
339    if id_str == "___death" {
340        return Ok(CoreCorticalType::Death.to_cortical_id());
341    }
342    if id_str == "___fatig" {
343        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
344    }
345    if id_str == "___pain_" {
346        return Ok(CoreCorticalType::Pain.to_cortical_id());
347    }
348    if id_str == "___pleas" {
349        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
350    }
351    if id_str == "___fear_" {
352        return Ok(CoreCorticalType::Fear.to_cortical_id());
353    }
354    if id_str == "___hope_" {
355        return Ok(CoreCorticalType::Hope.to_cortical_id());
356    }
357    if id_str == "_death" {
358        return Ok(CoreCorticalType::Death.to_cortical_id());
359    }
360    if id_str == "_fatigue" {
361        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
362    }
363    if id_str == "_pain" {
364        return Ok(CoreCorticalType::Pain.to_cortical_id());
365    }
366    if id_str == "_pleasure" {
367        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
368    }
369    if id_str == "_fear" {
370        return Ok(CoreCorticalType::Fear.to_cortical_id());
371    }
372    if id_str == "_hope" {
373        return Ok(CoreCorticalType::Hope.to_cortical_id());
374    }
375
376    // For non-core areas, use CorticalID's legacy ASCII parser (6-char and 8-char)
377    if id_str.len() == 6 || id_str.len() == 8 {
378        CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
379            EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
380        })
381    } else {
382        Err(EvoError::InvalidArea(format!(
383            "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
384            id_str
385        )))
386    }
387}
388
389/// Genome parser
390pub struct GenomeParser;
391
392/// Kernel/class/mask area bindings, kernel size, and class count after training-mode normalization.
393type NormalizedClassifierTraining = (
394    Option<String>,
395    Option<String>,
396    Option<String>,
397    Option<[u32; 3]>,
398    Option<u32>,
399);
400
401impl GenomeParser {
402    /// Normalize cortical ID list properties (inputs, outputs, designated_*) to base64 strings.
403    fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
404        for key in keys {
405            let Some(val) = region.get_property(key) else {
406                continue;
407            };
408            let Some(arr) = val.as_array() else {
409                continue;
410            };
411            let mut out: Vec<String> = Vec::new();
412            for item in arr {
413                let Some(s) = item.as_str() else {
414                    continue;
415                };
416                match string_to_cortical_id(s) {
417                    Ok(cortical_id) => out.push(cortical_id.as_base_64()),
418                    Err(e) => {
419                        warn!(target: "feagi-evo",
420                            "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
421                            key, s, e);
422                    }
423                }
424            }
425            if out.is_empty() {
426                region.properties.remove(*key);
427            } else {
428                region.add_property((*key).to_string(), serde_json::json!(out));
429            }
430        }
431    }
432
433    /// Parse a genome JSON string into a ParsedGenome
434    ///
435    /// # Arguments
436    ///
437    /// * `json_str` - JSON string of the genome
438    ///
439    /// # Returns
440    ///
441    /// Parsed genome ready for loading into ConnectomeManager
442    ///
443    /// # Errors
444    ///
445    /// Returns error if:
446    /// - JSON is malformed
447    /// - Required fields are missing
448    /// - Data types are invalid
449    ///
450    pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
451        // Deserialize raw genome
452        let raw: RawGenome = serde_json::from_str(json_str)
453            .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
454
455        // Validate version - support 2.x and 3.x (3.0 is flat format with base64 IDs)
456        if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
457            return Err(EvoError::InvalidGenome(format!(
458                "Unsupported genome version: {}. Expected 2.x or 3.x",
459                raw.version
460            )));
461        }
462
463        // Parse cortical areas from blueprint
464        let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
465
466        // Parse brain regions
467        let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
468        let classifiers = Self::parse_classifiers(&raw.classifiers)?;
469
470        Ok(ParsedGenome {
471            genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
472            genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
473            version: raw.version,
474            cortical_areas,
475            brain_regions,
476            classifiers,
477            neuron_morphologies: raw.neuron_morphologies,
478            physiology: raw.physiology,
479            change_history: raw.change_history,
480        })
481    }
482
483    /// Parse cortical areas from blueprint
484    fn parse_cortical_areas(
485        blueprint: &HashMap<String, RawCorticalArea>,
486    ) -> EvoResult<Vec<CorticalArea>> {
487        let mut areas = Vec::with_capacity(blueprint.len());
488
489        for (cortical_id_str, raw_area) in blueprint.iter() {
490            // Skip empty IDs
491            if cortical_id_str.is_empty() {
492                warn!(target: "feagi-evo","Skipping empty cortical_id");
493                continue;
494            }
495
496            // Convert string cortical_id to CorticalID (handles 6-char legacy and base64)
497            let cortical_id = match string_to_cortical_id(cortical_id_str) {
498                Ok(id) => id,
499                Err(e) => {
500                    warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
501                    continue;
502                }
503            };
504
505            // Extract required fields
506            let name = raw_area
507                .cortical_name
508                .clone()
509                .unwrap_or_else(|| cortical_id_str.clone());
510
511            let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
512                if boundaries.len() != 3 {
513                    return Err(EvoError::InvalidArea(format!(
514                        "Invalid block_boundaries for {}: expected 3 values, got {}",
515                        cortical_id_str,
516                        boundaries.len()
517                    )));
518                }
519                Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
520                    .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
521            } else {
522                // Default to 1x1x1 if not specified (should not happen in valid genomes)
523                warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
524                Dimensions::new(1, 1, 1).map_err(|e| {
525                    EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
526                })?
527            };
528
529            let position = if let Some(coords) = &raw_area.relative_coordinate {
530                if coords.len() != 3 {
531                    return Err(EvoError::InvalidArea(format!(
532                        "Invalid relative_coordinate for {}: expected 3 values, got {}",
533                        cortical_id_str,
534                        coords.len()
535                    )));
536                }
537                GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
538            } else {
539                // Default to origin if not specified
540                warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
541                GenomeCoordinate3D::new(0, 0, 0)
542            };
543
544            // Determine cortical type from cortical_id
545            let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
546                EvoError::InvalidArea(format!(
547                    "Failed to determine cortical type from ID {}: {}",
548                    cortical_id_str, e
549                ))
550            })?;
551
552            // Create cortical area with CorticalID object (zero-copy, type-safe)
553            let mut area = CorticalArea::new(
554                cortical_id,
555                0, // cortical_idx will be assigned by ConnectomeManager
556                name,
557                dimensions,
558                position,
559                cortical_type,
560            )?;
561
562            // Store cortical_type as cortical_group for new type system
563            if let Some(ref cortical_type_str) = raw_area.cortical_type {
564                area.properties.insert(
565                    "cortical_group".to_string(),
566                    serde_json::json!(cortical_type_str),
567                );
568            }
569
570            // Store all properties in the properties HashMap
571            // Neural properties
572            if let Some(v) = raw_area.synapse_attractivity {
573                area.properties
574                    .insert("synapse_attractivity".to_string(), serde_json::json!(v));
575            }
576            if let Some(v) = raw_area.refractory_period {
577                area.properties
578                    .insert("refractory_period".to_string(), serde_json::json!(v));
579            }
580            if let Some(v) = raw_area.firing_threshold {
581                area.properties
582                    .insert("firing_threshold".to_string(), serde_json::json!(v));
583            }
584            if let Some(v) = raw_area.firing_threshold_limit {
585                area.properties
586                    .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
587            }
588            if let Some(v) = raw_area.firing_threshold_increment_x {
589                area.properties.insert(
590                    "firing_threshold_increment_x".to_string(),
591                    serde_json::json!(v),
592                );
593            }
594            if let Some(v) = raw_area.firing_threshold_increment_y {
595                area.properties.insert(
596                    "firing_threshold_increment_y".to_string(),
597                    serde_json::json!(v),
598                );
599            }
600            if let Some(v) = raw_area.firing_threshold_increment_z {
601                area.properties.insert(
602                    "firing_threshold_increment_z".to_string(),
603                    serde_json::json!(v),
604                );
605            }
606            if let Some(v) = raw_area.leak_coefficient {
607                area.properties
608                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
609            }
610            if let Some(v) = raw_area.leak_variability {
611                area.properties
612                    .insert("leak_variability".to_string(), serde_json::json!(v));
613            }
614            if let Some(v) = raw_area.neuron_excitability {
615                area.properties
616                    .insert("neuron_excitability".to_string(), serde_json::json!(v));
617            }
618            if let Some(v) = raw_area.postsynaptic_current {
619                area.properties
620                    .insert("postsynaptic_current".to_string(), serde_json::json!(v));
621            }
622            if let Some(v) = raw_area.postsynaptic_current_max {
623                area.properties
624                    .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
625            }
626            if let Some(v) = raw_area.degeneration {
627                area.properties
628                    .insert("degeneration".to_string(), serde_json::json!(v));
629            }
630
631            // Boolean properties
632            if let Some(v) = raw_area.psp_uniform_distribution {
633                area.properties
634                    .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
635            }
636            if let Some(v) = raw_area.mp_charge_accumulation {
637                area.properties
638                    .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
639            }
640            if let Some(v) = raw_area.mp_driven_psp {
641                area.properties
642                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
643                tracing::info!(
644                    target: "feagi-evo",
645                    "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
646                    v,
647                    cortical_id_str
648                );
649            } else {
650                tracing::debug!(
651                    target: "feagi-evo",
652                    "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
653                    cortical_id_str
654                );
655            }
656            if let Some(v) = raw_area.visualization {
657                area.properties
658                    .insert("visualization".to_string(), serde_json::json!(v));
659                // Also store as "visible" for compatibility with getters
660                area.properties
661                    .insert("visible".to_string(), serde_json::json!(v));
662            }
663            if let Some(v) = raw_area.burst_engine_activation {
664                area.properties
665                    .insert("burst_engine_active".to_string(), serde_json::json!(v));
666            }
667            if let Some(v) = raw_area.is_mem_type {
668                area.properties
669                    .insert("is_mem_type".to_string(), serde_json::json!(v));
670            }
671
672            // Memory properties
673            if let Some(v) = raw_area.longterm_mem_threshold {
674                area.properties
675                    .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
676            }
677            if let Some(v) = raw_area.lifespan_growth_rate {
678                area.properties
679                    .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
680            }
681            if let Some(v) = raw_area.init_lifespan {
682                area.properties
683                    .insert("init_lifespan".to_string(), serde_json::json!(v));
684            }
685            if let Some(v) = raw_area.temporal_depth {
686                area.properties
687                    .insert("temporal_depth".to_string(), serde_json::json!(v));
688            }
689            if let Some(v) = raw_area.mp_learning_enabled {
690                area.properties
691                    .insert("mp_learning_enabled".to_string(), serde_json::json!(v));
692            }
693            if let Some(v) = &raw_area.mp_change_mode {
694                area.properties
695                    .insert("mp_change_mode".to_string(), serde_json::json!(v));
696            }
697            if let Some(v) = raw_area.mp_delta_quantization {
698                area.properties
699                    .insert("mp_delta_quantization".to_string(), serde_json::json!(v));
700            }
701            if let Some(v) = raw_area.mp_ratio_quantization {
702                area.properties
703                    .insert("mp_ratio_quantization".to_string(), serde_json::json!(v));
704            }
705            if let Some(v) = raw_area.min_window_activity {
706                area.properties
707                    .insert("min_window_activity".to_string(), serde_json::json!(v));
708            }
709            if let Some(v) = raw_area.scan_skip_density {
710                area.properties
711                    .insert("scan_skip_density".to_string(), serde_json::json!(v));
712            }
713            if let Some(v) = raw_area.consecutive_fire_cnt_max {
714                area.properties
715                    .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
716                // Also store as "consecutive_fire_limit" for getter compatibility
717                area.properties
718                    .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
719            }
720            if let Some(v) = raw_area.snooze_length {
721                area.properties
722                    .insert("snooze_period".to_string(), serde_json::json!(v));
723            }
724
725            // Other properties
726            if let Some(v) = &raw_area.group_id {
727                area.properties
728                    .insert("group_id".to_string(), serde_json::json!(v));
729            }
730            if let Some(v) = &raw_area.sub_group_id {
731                area.properties
732                    .insert("sub_group_id".to_string(), serde_json::json!(v));
733            }
734            // Store neurons_per_voxel in properties HashMap
735            if let Some(v) = raw_area.per_voxel_neuron_cnt {
736                area.properties
737                    .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
738            }
739            if let Some(v) = &raw_area.cortical_mapping_dst {
740                // Convert dstmap keys from old format to base64
741                let converted_dstmap = convert_dstmap_keys_to_base64(v);
742                area.properties
743                    .insert("cortical_mapping_dst".to_string(), converted_dstmap);
744            }
745            if let Some(v) = &raw_area.coordinate_2d {
746                area.properties
747                    .insert("2d_coordinate".to_string(), serde_json::json!(v));
748            }
749
750            // Store any other custom properties
751            for (key, value) in &raw_area.other {
752                area.properties.insert(key.clone(), value.clone());
753            }
754
755            // Note: cortical_type parsing disabled - CorticalArea is now a minimal data structure
756            // CorticalAreaType information is stored in properties["cortical_group"] if needed
757
758            areas.push(area);
759        }
760
761        Ok(areas)
762    }
763
764    /// Parse brain regions
765    fn parse_brain_regions(
766        raw_regions: &HashMap<String, RawBrainRegion>,
767    ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
768        let mut regions = Vec::with_capacity(raw_regions.len());
769
770        for (region_id_str, raw_region) in raw_regions.iter() {
771            let title = raw_region
772                .title
773                .clone()
774                .unwrap_or_else(|| region_id_str.clone());
775
776            // Convert string region_id to RegionID (UUID)
777            // For now, try to parse as UUID if it's already a UUID, otherwise generate new one
778            let region_id = match RegionID::from_string(region_id_str) {
779                Ok(id) => id,
780                Err(_) => {
781                    // If not a valid UUID, generate a new one
782                    // This handles legacy string-based region IDs
783                    RegionID::new()
784                }
785            };
786
787            let region_type = RegionType::Undefined; // Default to Undefined
788
789            let mut region = BrainRegion::new(region_id, title, region_type)?;
790
791            // v3 RuntimeGenome sections nest IO under `properties`; merge before list fields.
792            if let Some(props) = &raw_region.properties {
793                for (k, v) in props {
794                    region.add_property(k.clone(), v.clone());
795                }
796            }
797
798            // Add cortical areas to region (using CorticalID directly)
799            if let Some(areas) = &raw_region.areas {
800                for area_id in areas {
801                    // Convert area_id to CorticalID
802                    match string_to_cortical_id(area_id) {
803                        Ok(cortical_id) => {
804                            region.add_area(cortical_id);
805                        }
806                        Err(e) => {
807                            warn!(target: "feagi-evo",
808                                "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
809                                area_id, e);
810                        }
811                    }
812                }
813            }
814
815            // Store properties in HashMap
816            if let Some(desc) = &raw_region.description {
817                region.add_property("description".to_string(), serde_json::json!(desc));
818            }
819            if let Some(coord_2d) = &raw_region.coordinate_2d {
820                region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
821            }
822            if let Some(coord_3d) = &raw_region.coordinate_3d {
823                region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
824            }
825            // Store inputs/outputs as base64 strings
826            if let Some(inputs) = &raw_region.inputs {
827                let input_ids: Vec<String> = inputs
828                    .iter()
829                    .filter_map(|id| match string_to_cortical_id(id) {
830                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
831                        Err(e) => {
832                            warn!(target: "feagi-evo",
833                                    "Failed to convert brain region input ID '{}': {}. Skipping.",
834                                    id, e);
835                            None
836                        }
837                    })
838                    .collect();
839                if !input_ids.is_empty() {
840                    region.add_property("inputs".to_string(), serde_json::json!(input_ids));
841                }
842            }
843            if let Some(outputs) = &raw_region.outputs {
844                let output_ids: Vec<String> = outputs
845                    .iter()
846                    .filter_map(|id| match string_to_cortical_id(id) {
847                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
848                        Err(e) => {
849                            warn!(target: "feagi-evo",
850                                    "Failed to convert brain region output ID '{}': {}. Skipping.",
851                                    id, e);
852                            None
853                        }
854                    })
855                    .collect();
856                if !output_ids.is_empty() {
857                    region.add_property("outputs".to_string(), serde_json::json!(output_ids));
858                }
859            }
860            if let Some(signature) = &raw_region.signature {
861                region.add_property("signature".to_string(), serde_json::json!(signature));
862            }
863
864            if let Some(d) = &raw_region.designated_inputs {
865                let ids: Vec<String> = d
866                    .iter()
867                    .filter_map(|id| match string_to_cortical_id(id) {
868                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
869                        Err(e) => {
870                            warn!(target: "feagi-evo",
871                                "Failed to convert designated_inputs entry '{}': {}. Skipping.",
872                                id, e);
873                            None
874                        }
875                    })
876                    .collect();
877                if !ids.is_empty() {
878                    region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
879                }
880            }
881            if let Some(d) = &raw_region.designated_outputs {
882                let ids: Vec<String> = d
883                    .iter()
884                    .filter_map(|id| match string_to_cortical_id(id) {
885                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
886                        Err(e) => {
887                            warn!(target: "feagi-evo",
888                                "Failed to convert designated_outputs entry '{}': {}. Skipping.",
889                                id, e);
890                            None
891                        }
892                    })
893                    .collect();
894                if !ids.is_empty() {
895                    region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
896                }
897            }
898
899            Self::normalize_brain_region_cortical_id_list_properties(
900                &mut region,
901                &[
902                    "inputs",
903                    "outputs",
904                    "designated_inputs",
905                    "designated_outputs",
906                ],
907            );
908
909            // Store parent_id for hierarchy construction
910            let parent_id = raw_region.parent_region_id.clone();
911            if let Some(ref parent_id_str) = parent_id {
912                // Store as property for serialization
913                region.add_property(
914                    "parent_region_id".to_string(),
915                    serde_json::json!(parent_id_str),
916                );
917            }
918
919            regions.push((region, parent_id));
920        }
921
922        Ok(regions)
923    }
924
925    fn normalize_classifier_training(
926        classifier_id: &str,
927        training_mode: feagi_structures::genomic::classifiers::ClassifierTrainingMode,
928        kernel_area_id: Option<String>,
929        class_area_id: Option<String>,
930        mask_area_id: Option<String>,
931        kernel_size: Option<[u32; 3]>,
932        class_count: Option<u32>,
933    ) -> EvoResult<NormalizedClassifierTraining> {
934        use feagi_structures::genomic::classifiers::ClassifierTrainingMode;
935        match training_mode {
936            ClassifierTrainingMode::Kernel => {
937                if mask_area_id.is_some() || kernel_size.is_some() || class_count.is_some() {
938                    return Err(EvoError::InvalidArea(format!(
939                    "Classifier '{classifier_id}' is in kernel mode and cannot store a mask, kernel size, or class count"
940                )));
941                }
942                Ok((kernel_area_id, class_area_id, None, None, None))
943            }
944            ClassifierTrainingMode::Scanner => {
945                if kernel_area_id.is_some() || class_area_id.is_some() {
946                    return Err(EvoError::InvalidArea(format!(
947                    "Classifier '{classifier_id}' is in scanner mode and cannot store kernel or class areas"
948                )));
949                }
950                let mask = mask_area_id
951                    .filter(|id| !id.trim().is_empty())
952                    .ok_or_else(|| {
953                        EvoError::InvalidArea(format!(
954                    "Classifier '{classifier_id}' is in scanner mode and is missing mask_area_id"
955                ))
956                    })?;
957                let size = kernel_size.ok_or_else(|| {
958                    EvoError::InvalidArea(format!(
959                    "Classifier '{classifier_id}' is in scanner mode and is missing kernel_size"
960                ))
961                })?;
962                feagi_structures::genomic::classifiers::validate_kernel_size(size).map_err(
963                    |e| EvoError::InvalidArea(format!("Classifier '{classifier_id}' {e}")),
964                )?;
965                let count = class_count.ok_or_else(|| {
966                    EvoError::InvalidArea(format!(
967                    "Classifier '{classifier_id}' is in scanner mode and is missing class_count"
968                ))
969                })?;
970                feagi_structures::neuron_voxels::class_potential::validate_class_count(count)
971                    .map_err(|e| {
972                        EvoError::InvalidArea(format!("Classifier '{classifier_id}' {e}"))
973                    })?;
974                Ok((None, None, Some(mask), Some(size), Some(count)))
975            }
976        }
977    }
978
979    fn parse_classifiers(
980        raw_classifiers: &HashMap<String, RawClassifier>,
981    ) -> EvoResult<Vec<Classifier>> {
982        let mut classifiers = Vec::with_capacity(raw_classifiers.len());
983        for (classifier_id, raw) in raw_classifiers {
984            let name = raw
985                .name
986                .clone()
987                .filter(|n| !n.trim().is_empty())
988                .ok_or_else(|| {
989                    EvoError::InvalidArea(format!("Classifier '{}' is missing name", classifier_id))
990                })?;
991            let parent_region_id = raw
992                .parent_region_id
993                .clone()
994                .filter(|n| !n.trim().is_empty())
995                .ok_or_else(|| {
996                    EvoError::InvalidArea(format!(
997                        "Classifier '{}' is missing parent_region_id",
998                        classifier_id
999                    ))
1000                })?;
1001            let coordinates_3d = match &raw.coordinates_3d {
1002                Some(coords) if coords.len() == 3 => [coords[0], coords[1], coords[2]],
1003                Some(coords) => {
1004                    return Err(EvoError::InvalidArea(format!(
1005                        "Classifier '{}' coordinates_3d must have 3 values, got {}",
1006                        classifier_id,
1007                        coords.len()
1008                    )))
1009                }
1010                None => [0, 0, 0],
1011            };
1012            let kernel_memory_id = raw.kernel_memory_id.clone().ok_or_else(|| {
1013                EvoError::InvalidArea(format!(
1014                    "Classifier '{}' is missing kernel_memory_id",
1015                    classifier_id
1016                ))
1017            })?;
1018            let class_memory_id = raw.class_memory_id.clone().ok_or_else(|| {
1019                EvoError::InvalidArea(format!(
1020                    "Classifier '{}' is missing class_memory_id",
1021                    classifier_id
1022                ))
1023            })?;
1024            let fields = classifier_fields_from_raw(raw);
1025            let training_mode = raw.training_mode.unwrap_or_default();
1026            let (kernel_area_id, class_area_id, mask_area_id, kernel_size, class_count) =
1027                Self::normalize_classifier_training(
1028                    classifier_id,
1029                    training_mode,
1030                    raw.kernel_area_id.clone(),
1031                    raw.class_area_id.clone(),
1032                    raw.mask_area_id.clone(),
1033                    raw.kernel_size,
1034                    raw.class_count,
1035                )?;
1036            classifiers.push(Classifier {
1037                classifier_id: classifier_id.clone(),
1038                name,
1039                parent_region_id,
1040                coordinates_3d,
1041                training_mode,
1042                kernel_area_id,
1043                class_area_id,
1044                mask_area_id,
1045                class_count,
1046                kernel_size,
1047                fields,
1048                kernel_memory_id,
1049                class_memory_id,
1050                reward_training: raw.reward_training,
1051                answer_feedback_area_id: raw.answer_feedback_area_id.clone(),
1052                pain_area_id: raw.pain_area_id.clone(),
1053                pleasure_area_id: raw.pleasure_area_id.clone(),
1054                answer_latency_bursts: raw.answer_latency_bursts,
1055                learn_area_id: raw.learn_area_id.clone(),
1056                confidence_area_id: raw.confidence_area_id.clone(),
1057                properties: raw.properties.clone().unwrap_or_default(),
1058            });
1059        }
1060        Ok(classifiers)
1061    }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066    use super::*;
1067
1068    #[test]
1069    fn test_parse_minimal_genome() {
1070        // Test backward compatibility: parsing v2.1 genome with old 6-byte cortical ID
1071        // Parser should convert old format to base64 for storage
1072        let json = r#"{
1073            "version": "2.1",
1074            "blueprint": {
1075                "_power": {
1076                    "cortical_name": "Test Area",
1077                    "block_boundaries": [10, 10, 10],
1078                    "relative_coordinate": [0, 0, 0],
1079                    "cortical_type": "CORE"
1080                }
1081            },
1082            "brain_regions": {
1083                "root": {
1084                    "title": "Root",
1085                    "parent_region_id": null,
1086                    "areas": ["_power"]
1087                }
1088            }
1089        }"#;
1090
1091        let parsed = GenomeParser::parse(json).unwrap();
1092
1093        assert_eq!(parsed.version, "2.1");
1094        assert_eq!(parsed.cortical_areas.len(), 1);
1095        // Input was "_power" (6 bytes), converted to "___power" (8 bytes, padded at start with underscores) then base64 encoded
1096        assert_eq!(
1097            parsed.cortical_areas[0].cortical_id.as_base_64(),
1098            "X19fcG93ZXI="
1099        );
1100        assert_eq!(parsed.cortical_areas[0].name, "Test Area");
1101        assert_eq!(parsed.brain_regions.len(), 1);
1102
1103        // Phase 2: Verify cortical_type_new is populated
1104        // Note: cortical_type_new field removed - type is encoded in cortical_id
1105        assert!(parsed.cortical_areas[0]
1106            .cortical_id
1107            .as_cortical_type()
1108            .is_ok());
1109    }
1110
1111    #[test]
1112    fn test_parse_multiple_areas() {
1113        // Test parsing multiple cortical areas with old format IDs
1114        let json = r#"{
1115            "version": "2.1",
1116            "blueprint": {
1117                "_power": {
1118                    "cortical_name": "Area 1",
1119                    "cortical_type": "CORE",
1120                    "block_boundaries": [5, 5, 5],
1121                    "relative_coordinate": [0, 0, 0]
1122                },
1123                "_death": {
1124                    "cortical_name": "Area 2",
1125                    "cortical_type": "CORE",
1126                    "block_boundaries": [10, 10, 10],
1127                    "relative_coordinate": [5, 0, 0]
1128                }
1129            }
1130        }"#;
1131
1132        let parsed = GenomeParser::parse(json).unwrap();
1133        assert!(parsed.classifiers.is_empty());
1134
1135        assert_eq!(parsed.cortical_areas.len(), 2);
1136
1137        // Phase 2: Verify both areas have cortical_type_new populated
1138        for area in &parsed.cortical_areas {
1139            assert!(
1140                area.cortical_id.as_cortical_type().is_ok(),
1141                "Area {} should have cortical_type_new populated",
1142                area.cortical_id
1143            );
1144        }
1145    }
1146
1147    #[test]
1148    fn test_string_to_cortical_id_legacy_power_shorthand() {
1149        // Older FEAGI genomes may encode the power core area as "___pwr" (6-char shorthand).
1150        // Migration must map this deterministically to the core Power cortical ID.
1151        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1152        let id = string_to_cortical_id("___pwr").unwrap();
1153        assert_eq!(
1154            id.as_base_64(),
1155            CoreCorticalType::Power.to_cortical_id().as_base_64()
1156        );
1157    }
1158
1159    #[test]
1160    fn test_parse_raw_imu_magnetometer_wire_id() {
1161        // Embodiment-registered Raw IMU magnetometer (subunit 2) from live FEAGI.
1162        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1163        let json = r#"{
1164            "version": "3.0",
1165            "blueprint": {
1166                "aXJpbScAAgA=": {
1167                    "cortical_name": "feagi_body_imu__Abdomen-2",
1168                    "block_boundaries": [3, 1, 10],
1169                    "relative_coordinate": [90, 0, -10],
1170                    "cortical_type": "IPU"
1171                }
1172            },
1173            "brain_regions": {}
1174        }"#;
1175
1176        let parsed = GenomeParser::parse(json).expect("Raw IMU magnetometer genome");
1177        assert_eq!(parsed.cortical_areas.len(), 1);
1178        assert_eq!(
1179            parsed.cortical_areas[0].cortical_id.as_base_64(),
1180            "aXJpbScAAgA="
1181        );
1182        parsed.cortical_areas[0]
1183            .cortical_id
1184            .as_cortical_type()
1185            .expect("magnetometer IO flag must decode");
1186    }
1187
1188    #[test]
1189    fn test_parse_positional_servo_speed_wire_id() {
1190        // Embodiment-registered Positional Servo Speed (subunit 2) from live FEAGI.
1191        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1192        let json = r#"{
1193            "version": "3.0",
1194            "blueprint": {
1195                "b3BzZSEAAAA=": {
1196                    "cortical_name": "Positional Servo Speed",
1197                    "block_boundaries": [6, 1, 20],
1198                    "relative_coordinate": [-58, 0, -10],
1199                    "cortical_type": "OPU"
1200                }
1201            },
1202            "brain_regions": {}
1203        }"#;
1204
1205        let parsed = GenomeParser::parse(json).expect("Positional Servo Speed genome");
1206        assert_eq!(parsed.cortical_areas.len(), 1);
1207        assert_eq!(
1208            parsed.cortical_areas[0].cortical_id.as_base_64(),
1209            "b3BzZSEAAAA="
1210        );
1211        parsed.cortical_areas[0]
1212            .cortical_id
1213            .as_cortical_type()
1214            .expect("positional servo speed IO flag must decode");
1215    }
1216
1217    #[test]
1218    fn test_string_to_cortical_id_legacy_power_padded() {
1219        // 8-char padded form ___pwr__ (from 6-char padding in legacy flat genomes).
1220        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1221        let id = string_to_cortical_id("___pwr__").unwrap();
1222        assert_eq!(
1223            id.as_base_64(),
1224            CoreCorticalType::Power.to_cortical_id().as_base_64()
1225        );
1226    }
1227
1228    #[test]
1229    fn test_parse_with_properties() {
1230        let json = r#"{
1231            "version": "2.1",
1232            "blueprint": {
1233                "mem001": {
1234                    "cortical_name": "Memory Area",
1235                    "block_boundaries": [8, 8, 8],
1236                    "relative_coordinate": [0, 0, 0],
1237                    "cortical_type": "MEMORY",
1238                    "is_mem_type": true,
1239                    "firing_threshold": 50.0,
1240                    "leak_coefficient": 0.9
1241                }
1242            }
1243        }"#;
1244
1245        let parsed = GenomeParser::parse(json).unwrap();
1246
1247        assert_eq!(parsed.cortical_areas.len(), 1);
1248        let area = &parsed.cortical_areas[0];
1249
1250        // Old type system (deprecated)
1251        use feagi_structures::genomic::cortical_area::CorticalAreaType;
1252        assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
1253
1254        // Properties stored correctly
1255        assert!(area.properties.contains_key("is_mem_type"));
1256        assert!(area.properties.contains_key("firing_threshold"));
1257        assert!(area.properties.contains_key("cortical_group"));
1258
1259        // NEW: cortical_type should be derivable from cortical_id (Phase 2)
1260        assert!(
1261            area.cortical_id.as_cortical_type().is_ok(),
1262            "cortical_id should be parseable to cortical_type"
1263        );
1264        if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
1265            use feagi_structures::genomic::cortical_area::CorticalAreaType;
1266            assert!(
1267                matches!(cortical_type, CorticalAreaType::Memory(_)),
1268                "Should be classified as MEMORY type"
1269            );
1270        }
1271    }
1272
1273    /// v3 save embeds IO lists under `properties`; loading must preserve designated_inputs for BV presets.
1274    #[test]
1275    fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
1276        let json = r#"{
1277            "version": "3.0",
1278            "blueprint": {
1279                "_power": {
1280                    "cortical_name": "Core",
1281                    "block_boundaries": [10, 10, 10],
1282                    "relative_coordinate": [0, 0, 0],
1283                    "cortical_type": "CORE"
1284                }
1285            },
1286            "brain_regions": {
1287                "550e8400-e29b-41d4-a716-446655440000": {
1288                    "name": "Sub",
1289                    "cortical_areas": ["_power"],
1290                    "properties": {
1291                        "designated_inputs": ["_power"],
1292                        "designated_outputs": []
1293                    }
1294                }
1295            }
1296        }"#;
1297
1298        let parsed = GenomeParser::parse(json).unwrap();
1299        assert_eq!(parsed.brain_regions.len(), 1);
1300        let (region, _) = &parsed.brain_regions[0];
1301        let di = region
1302            .get_property("designated_inputs")
1303            .and_then(|v| v.as_array())
1304            .expect("designated_inputs");
1305        assert_eq!(di.len(), 1);
1306        assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
1307    }
1308
1309    #[test]
1310    fn test_parse_brain_region_plain_text_description() {
1311        let json = r#"{
1312            "version": "2.1",
1313            "blueprint": {
1314                "_power": {
1315                    "cortical_name": "Core",
1316                    "block_boundaries": [10, 10, 10],
1317                    "relative_coordinate": [0, 0, 0],
1318                    "cortical_type": "CORE"
1319                }
1320            },
1321            "brain_regions": {
1322                "root": {
1323                    "title": "Root",
1324                    "description": "Holds core physiology and embodiment IO",
1325                    "parent_region_id": null,
1326                    "areas": ["_power"]
1327                }
1328            }
1329        }"#;
1330
1331        let parsed = GenomeParser::parse(json).unwrap();
1332        assert_eq!(parsed.brain_regions.len(), 1);
1333        let (region, _) = &parsed.brain_regions[0];
1334        assert_eq!(
1335            region.get_property("description"),
1336            Some(&serde_json::json!(
1337                "Holds core physiology and embodiment IO"
1338            ))
1339        );
1340    }
1341
1342    #[test]
1343    fn test_invalid_version() {
1344        let json = r#"{
1345            "version": "1.0",
1346            "blueprint": {}
1347        }"#;
1348
1349        let result = GenomeParser::parse(json);
1350        assert!(result.is_err());
1351    }
1352
1353    #[test]
1354    fn test_malformed_json() {
1355        let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
1356
1357        let result = GenomeParser::parse(json);
1358        assert!(result.is_err());
1359    }
1360
1361    #[test]
1362    fn test_cortical_type_new_population() {
1363        // Test that cortical_type_new field is populated during parsing (Phase 2)
1364        // This tests that parsing works with valid cortical IDs and populates types correctly
1365        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1366        let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
1367        let json = format!(
1368            r#"{{
1369            "version": "2.1",
1370            "blueprint": {{
1371                "cvision1": {{
1372                    "cortical_name": "Test Custom Vision",
1373                    "cortical_type": "CUSTOM",
1374                    "block_boundaries": [10, 10, 1],
1375                    "relative_coordinate": [0, 0, 0]
1376                }},
1377                "cmotor01": {{
1378                    "cortical_name": "Test Custom Motor",
1379                    "cortical_type": "CUSTOM",
1380                    "block_boundaries": [5, 5, 1],
1381                    "relative_coordinate": [0, 0, 0]
1382                }},
1383                "{}": {{
1384                    "cortical_name": "Test Core",
1385                    "cortical_type": "CORE",
1386                    "block_boundaries": [1, 1, 1],
1387                    "relative_coordinate": [0, 0, 0]
1388                }}
1389            }}
1390        }}"#,
1391            power_id
1392        );
1393
1394        let parsed = GenomeParser::parse(&json).unwrap();
1395        assert_eq!(parsed.cortical_areas.len(), 3);
1396
1397        // Verify all areas have cortical_type_new populated
1398        for area in &parsed.cortical_areas {
1399            assert!(
1400                area.cortical_id.as_cortical_type().is_ok(),
1401                "Area {} should have cortical_type_new populated",
1402                area.cortical_id
1403            );
1404
1405            // Verify cortical_group property is also set
1406            assert!(
1407                area.properties.contains_key("cortical_group"),
1408                "Area {} should have cortical_group property",
1409                area.cortical_id
1410            );
1411
1412            // Verify cortical group is consistent (avoid depending on feagi-brain-development)
1413            if let Some(prop_group) = area
1414                .properties
1415                .get("cortical_group")
1416                .and_then(|v| v.as_str())
1417            {
1418                assert!(
1419                    !prop_group.is_empty(),
1420                    "Area {} should have non-empty cortical_group property",
1421                    area.cortical_id.as_base_64()
1422                );
1423            }
1424        }
1425    }
1426
1427    #[test]
1428    fn test_parse_classifiers_key_parallel_to_regions() {
1429        let json = r#"{
1430            "version": "3.0",
1431            "blueprint": {
1432                "cfield": {
1433                    "cortical_name": "Field",
1434                    "cortical_type": "CUSTOM",
1435                    "block_boundaries": [4, 4, 1],
1436                    "relative_coordinate": [0, 0, 0]
1437                },
1438                "mkmem1": {
1439                    "cortical_name": "KernelMem",
1440                    "cortical_type": "MEMORY",
1441                    "block_boundaries": [2, 2, 2],
1442                    "relative_coordinate": [10, 0, 0]
1443                },
1444                "mcmem1": {
1445                    "cortical_name": "ClassMem",
1446                    "cortical_type": "MEMORY",
1447                    "block_boundaries": [2, 2, 2],
1448                    "relative_coordinate": [20, 0, 0]
1449                },
1450                "cscan1": {
1451                    "cortical_name": "ScanTwin",
1452                    "cortical_type": "CUSTOM",
1453                    "block_boundaries": [4, 4, 3],
1454                    "relative_coordinate": [30, 0, 0]
1455                }
1456            },
1457            "brain_regions": {
1458                "root": {
1459                    "title": "root",
1460                    "parent_region_id": "",
1461                    "coordinate_2d": [0, 0],
1462                    "coordinate_3d": [0, 0, 0],
1463                    "areas": ["cfield", "mkmem1", "mcmem1", "cscan1"],
1464                    "regions": [],
1465                    "inputs": [],
1466                    "outputs": []
1467                }
1468            },
1469            "classifiers": {
1470                "clf-1": {
1471                    "name": "object_class",
1472                    "parent_region_id": "root",
1473                    "coordinates_3d": [30, 0, 0],
1474                    "field_area_id": "cfield",
1475                    "kernel_memory_id": "mkmem1",
1476                    "class_memory_id": "mcmem1",
1477                    "scan_twin_id": "cscan1"
1478                }
1479            }
1480        }"#;
1481
1482        let parsed = GenomeParser::parse(json).expect("classifier genome");
1483        assert_eq!(parsed.classifiers.len(), 1);
1484        let classifier = &parsed.classifiers[0];
1485        assert_eq!(classifier.classifier_id, "clf-1");
1486        assert_eq!(classifier.name, "object_class");
1487        assert_eq!(classifier.parent_region_id, "root");
1488        assert_eq!(classifier.fields.len(), 1);
1489        assert_eq!(classifier.fields[0].field_area_id, "cfield");
1490        assert_eq!(classifier.kernel_memory_id, "mkmem1");
1491        assert_eq!(classifier.class_memory_id, "mcmem1");
1492        assert_eq!(classifier.fields[0].scan_twin_id, "cscan1");
1493        assert_eq!(classifier.owned_area_ids().len(), 3);
1494        assert_eq!(
1495            classifier.training_mode,
1496            feagi_structures::genomic::classifiers::ClassifierTrainingMode::Kernel
1497        );
1498        assert!(classifier.mask_area_id.is_none());
1499        assert!(classifier.kernel_size.is_none());
1500    }
1501
1502    #[test]
1503    fn test_parse_scanner_classifier_round_trip_fields() {
1504        let json = r#"{
1505            "version": "3.0",
1506            "blueprint": {},
1507            "brain_regions": {},
1508            "classifiers": {
1509                "clf-scan": {
1510                    "name": "scan",
1511                    "parent_region_id": "root",
1512                    "coordinates_3d": [1, 2, 3],
1513                    "training_mode": "scanner",
1514                    "mask_area_id": "cmask",
1515                    "kernel_size": [8, 8, 3],
1516                    "class_count": 19,
1517                    "kernel_memory_id": "mkmem1",
1518                    "class_memory_id": "mcmem1"
1519                }
1520            }
1521        }"#;
1522        let parsed = GenomeParser::parse(json).expect("scanner classifier");
1523        let classifier = &parsed.classifiers[0];
1524        assert_eq!(
1525            classifier.training_mode,
1526            feagi_structures::genomic::classifiers::ClassifierTrainingMode::Scanner
1527        );
1528        assert_eq!(classifier.mask_area_id.as_deref(), Some("cmask"));
1529        assert_eq!(classifier.kernel_size, Some([8, 8, 3]));
1530        assert_eq!(classifier.class_count, Some(19));
1531        assert!(classifier.kernel_area_id.is_none());
1532    }
1533
1534    #[test]
1535    fn test_parse_scanner_classifier_without_class_count_is_rejected() {
1536        let json = r#"{
1537            "version": "3.0",
1538            "blueprint": {},
1539            "brain_regions": {},
1540            "classifiers": {
1541                "clf-scan": {
1542                    "name": "scan",
1543                    "parent_region_id": "root",
1544                    "training_mode": "scanner",
1545                    "mask_area_id": "cmask",
1546                    "kernel_size": [8, 8, 3],
1547                    "kernel_memory_id": "mkmem1",
1548                    "class_memory_id": "mcmem1"
1549                }
1550            }
1551        }"#;
1552        let error = GenomeParser::parse(json).expect_err("class_count is required");
1553        assert!(error.to_string().contains("class_count"), "{error}");
1554    }
1555}