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::cortical_area::CorticalID;
51use feagi_structures::genomic::cortical_area::{
52    CorticalArea, CorticalAreaDimensions as Dimensions,
53};
54use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
55use feagi_structures::genomic::{BrainRegion, RegionType};
56
57/// Parsed genome data ready for ConnectomeManager
58#[derive(Debug, Clone)]
59pub struct ParsedGenome {
60    /// Genome metadata
61    pub genome_id: String,
62    pub genome_title: String,
63    pub version: String,
64
65    /// Cortical areas extracted from blueprint
66    pub cortical_areas: Vec<CorticalArea>,
67
68    /// Brain regions and hierarchy
69    pub brain_regions: Vec<(BrainRegion, Option<String>)>, // (region, parent_id)
70
71    /// Raw neuron morphologies (for later processing)
72    pub neuron_morphologies: HashMap<String, Value>,
73
74    /// Raw physiology data (for later processing)
75    pub physiology: Option<Value>,
76}
77
78/// Raw genome JSON structure for deserialization
79#[derive(Debug, Clone, Deserialize, Serialize)]
80pub struct RawGenome {
81    pub genome_id: Option<String>,
82    pub genome_title: Option<String>,
83    pub genome_description: Option<String>,
84    pub version: String,
85    /// Integer schema version. Optional on the wire so older genomes that
86    /// pre-date this field still deserialize. The authoritative resolver
87    /// is `crate::genome::schema::detect_schema_version` and consumers
88    /// MUST go through it instead of branching on this field directly.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub genome_schema_version: Option<u32>,
91    pub blueprint: HashMap<String, RawCorticalArea>,
92    #[serde(default)]
93    pub brain_regions: HashMap<String, RawBrainRegion>,
94    #[serde(default)]
95    pub neuron_morphologies: HashMap<String, Value>,
96    #[serde(default)]
97    pub physiology: Option<Value>,
98    /// Root brain region ID (UUID string) - for O(1) root lookup
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub brain_regions_root: Option<String>,
101}
102
103/// Raw cortical area from blueprint
104#[derive(Debug, Clone, Deserialize, Serialize)]
105pub struct RawCorticalArea {
106    pub cortical_name: Option<String>,
107    pub block_boundaries: Option<Vec<u32>>,
108    pub relative_coordinate: Option<Vec<i32>>,
109    pub cortical_type: Option<String>,
110
111    // Optional properties
112    pub group_id: Option<String>,
113    pub sub_group_id: Option<String>,
114    pub per_voxel_neuron_cnt: Option<u32>,
115    pub cortical_mapping_dst: Option<Value>,
116
117    // Neural properties
118    pub synapse_attractivity: Option<f32>,
119    pub refractory_period: Option<u32>,
120    pub firing_threshold: Option<f32>,
121    pub firing_threshold_limit: Option<f32>,
122    pub firing_threshold_increment_x: Option<f32>,
123    pub firing_threshold_increment_y: Option<f32>,
124    pub firing_threshold_increment_z: Option<f32>,
125    pub leak_coefficient: Option<f32>,
126    pub leak_variability: Option<f32>,
127    pub neuron_excitability: Option<f32>,
128    pub postsynaptic_current: Option<f32>,
129    pub postsynaptic_current_max: Option<f32>,
130    pub degeneration: Option<f32>,
131    pub psp_uniform_distribution: Option<bool>,
132    pub mp_charge_accumulation: Option<bool>,
133    pub mp_driven_psp: Option<bool>,
134    pub visualization: Option<bool>,
135    pub burst_engine_activation: Option<bool>,
136    #[serde(rename = "2d_coordinate")]
137    pub coordinate_2d: Option<Vec<i32>>,
138
139    // Memory properties
140    pub is_mem_type: Option<bool>,
141    pub longterm_mem_threshold: Option<u32>,
142    pub lifespan_growth_rate: Option<f32>,
143    pub init_lifespan: Option<u32>,
144    pub temporal_depth: Option<u32>,
145    pub consecutive_fire_cnt_max: Option<u32>,
146    pub snooze_length: Option<u32>,
147
148    // Allow any other properties (future-proofing)
149    #[serde(flatten)]
150    pub other: HashMap<String, Value>,
151}
152
153/// Raw brain region from genome
154#[derive(Debug, Clone, Deserialize, Serialize)]
155pub struct RawBrainRegion {
156    #[serde(alias = "name")]
157    pub title: Option<String>,
158    pub description: Option<String>,
159    pub parent_region_id: Option<String>,
160    pub coordinate_2d: Option<Vec<i32>>,
161    pub coordinate_3d: Option<Vec<i32>>,
162    #[serde(alias = "cortical_areas")]
163    pub areas: Option<Vec<String>>,
164    pub regions: Option<Vec<String>>,
165    pub inputs: Option<Vec<String>>,
166    pub outputs: Option<Vec<String>>,
167    /// Declared interface lists (persisted from RuntimeGenome / PUT region).
168    pub designated_inputs: Option<Vec<String>>,
169    pub designated_outputs: Option<Vec<String>>,
170    pub signature: Option<String>,
171    /// v3 `serde_json::to_value(BrainRegion)` nests `inputs` / `designated_*` under `properties`.
172    pub properties: Option<HashMap<String, Value>>,
173}
174
175/// Convert cortical_mapping_dst keys from old format to base64
176///
177/// This ensures all destination cortical IDs in dstmap are stored in the new base64 format.
178fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
179    if let Some(dstmap_obj) = dstmap.as_object() {
180        let mut converted = serde_json::Map::new();
181
182        for (dest_id_str, mapping_value) in dstmap_obj {
183            // Convert destination cortical_id to base64 format
184            match string_to_cortical_id(dest_id_str) {
185                Ok(dest_cortical_id) => {
186                    converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
187                }
188                Err(e) => {
189                    // If conversion fails, keep original and log warning
190                    tracing::warn!(
191                        "Failed to convert dstmap key '{}' to base64: {}, keeping original",
192                        dest_id_str,
193                        e
194                    );
195                    converted.insert(dest_id_str.clone(), mapping_value.clone());
196                }
197            }
198        }
199
200        Value::Object(converted)
201    } else {
202        // Not an object, return as-is
203        dstmap.clone()
204    }
205}
206
207/// Convert a string cortical_id to CorticalID
208/// Handles both old 6-char format and new base64 format
209/// CRITICAL: Uses feagi-data-processing types as single source of truth for core areas
210pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
211    use feagi_structures::genomic::cortical_area::CoreCorticalType;
212
213    // Try base64 first (new format)
214    if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
215        let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
216        cortical_id.write_id_to_bytes(&mut bytes);
217        if bytes == *b"___power" {
218            return Ok(CoreCorticalType::Power.to_cortical_id());
219        }
220        if bytes == *b"___death" {
221            return Ok(CoreCorticalType::Death.to_cortical_id());
222        }
223        if bytes == *b"___fatig" {
224            return Ok(CoreCorticalType::Fatigue.to_cortical_id());
225        }
226        if bytes == *b"___pain_" {
227            return Ok(CoreCorticalType::Pain.to_cortical_id());
228        }
229        if bytes == *b"___pleas" {
230            return Ok(CoreCorticalType::Pleasure.to_cortical_id());
231        }
232        if bytes == *b"___fear_" {
233            return Ok(CoreCorticalType::Fear.to_cortical_id());
234        }
235        if bytes == *b"___hope_" {
236            return Ok(CoreCorticalType::Hope.to_cortical_id());
237        }
238        return Ok(cortical_id);
239    }
240
241    // Handle legacy CORE area names (6-char format) - use proper types from feagi-data-processing
242    if id_str == "_power" {
243        return Ok(CoreCorticalType::Power.to_cortical_id());
244    }
245    // Legacy shorthand used by older FEAGI genomes: "___pwr" (6-char) refers to core Power.
246    if id_str == "___pwr" {
247        return Ok(CoreCorticalType::Power.to_cortical_id());
248    }
249    // Legacy 8-char core names used in some BV caches
250    if id_str == "___power" {
251        return Ok(CoreCorticalType::Power.to_cortical_id());
252    }
253    // 8-char padded form of ___pwr (from 6-char padding in legacy flat genomes)
254    if id_str == "___pwr__" {
255        return Ok(CoreCorticalType::Power.to_cortical_id());
256    }
257    if id_str == "___death" {
258        return Ok(CoreCorticalType::Death.to_cortical_id());
259    }
260    if id_str == "___fatig" {
261        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
262    }
263    if id_str == "___pain_" {
264        return Ok(CoreCorticalType::Pain.to_cortical_id());
265    }
266    if id_str == "___pleas" {
267        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
268    }
269    if id_str == "___fear_" {
270        return Ok(CoreCorticalType::Fear.to_cortical_id());
271    }
272    if id_str == "___hope_" {
273        return Ok(CoreCorticalType::Hope.to_cortical_id());
274    }
275    if id_str == "_death" {
276        return Ok(CoreCorticalType::Death.to_cortical_id());
277    }
278    if id_str == "_fatigue" {
279        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
280    }
281    if id_str == "_pain" {
282        return Ok(CoreCorticalType::Pain.to_cortical_id());
283    }
284    if id_str == "_pleasure" {
285        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
286    }
287    if id_str == "_fear" {
288        return Ok(CoreCorticalType::Fear.to_cortical_id());
289    }
290    if id_str == "_hope" {
291        return Ok(CoreCorticalType::Hope.to_cortical_id());
292    }
293
294    // For non-core areas, use CorticalID's legacy ASCII parser (6-char and 8-char)
295    if id_str.len() == 6 || id_str.len() == 8 {
296        CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
297            EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
298        })
299    } else {
300        Err(EvoError::InvalidArea(format!(
301            "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
302            id_str
303        )))
304    }
305}
306
307/// Genome parser
308pub struct GenomeParser;
309
310impl GenomeParser {
311    /// Normalize cortical ID list properties (inputs, outputs, designated_*) to base64 strings.
312    fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
313        for key in keys {
314            let Some(val) = region.get_property(key) else {
315                continue;
316            };
317            let Some(arr) = val.as_array() else {
318                continue;
319            };
320            let mut out: Vec<String> = Vec::new();
321            for item in arr {
322                let Some(s) = item.as_str() else {
323                    continue;
324                };
325                match string_to_cortical_id(s) {
326                    Ok(cortical_id) => out.push(cortical_id.as_base_64()),
327                    Err(e) => {
328                        warn!(target: "feagi-evo",
329                            "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
330                            key, s, e);
331                    }
332                }
333            }
334            if out.is_empty() {
335                region.properties.remove(*key);
336            } else {
337                region.add_property((*key).to_string(), serde_json::json!(out));
338            }
339        }
340    }
341
342    /// Parse a genome JSON string into a ParsedGenome
343    ///
344    /// # Arguments
345    ///
346    /// * `json_str` - JSON string of the genome
347    ///
348    /// # Returns
349    ///
350    /// Parsed genome ready for loading into ConnectomeManager
351    ///
352    /// # Errors
353    ///
354    /// Returns error if:
355    /// - JSON is malformed
356    /// - Required fields are missing
357    /// - Data types are invalid
358    ///
359    pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
360        // Deserialize raw genome
361        let raw: RawGenome = serde_json::from_str(json_str)
362            .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
363
364        // Validate version - support 2.x and 3.x (3.0 is flat format with base64 IDs)
365        if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
366            return Err(EvoError::InvalidGenome(format!(
367                "Unsupported genome version: {}. Expected 2.x or 3.x",
368                raw.version
369            )));
370        }
371
372        // Parse cortical areas from blueprint
373        let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
374
375        // Parse brain regions
376        let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
377
378        Ok(ParsedGenome {
379            genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
380            genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
381            version: raw.version,
382            cortical_areas,
383            brain_regions,
384            neuron_morphologies: raw.neuron_morphologies,
385            physiology: raw.physiology,
386        })
387    }
388
389    /// Parse cortical areas from blueprint
390    fn parse_cortical_areas(
391        blueprint: &HashMap<String, RawCorticalArea>,
392    ) -> EvoResult<Vec<CorticalArea>> {
393        let mut areas = Vec::with_capacity(blueprint.len());
394
395        for (cortical_id_str, raw_area) in blueprint.iter() {
396            // Skip empty IDs
397            if cortical_id_str.is_empty() {
398                warn!(target: "feagi-evo","Skipping empty cortical_id");
399                continue;
400            }
401
402            // Convert string cortical_id to CorticalID (handles 6-char legacy and base64)
403            let cortical_id = match string_to_cortical_id(cortical_id_str) {
404                Ok(id) => id,
405                Err(e) => {
406                    warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
407                    continue;
408                }
409            };
410
411            // Extract required fields
412            let name = raw_area
413                .cortical_name
414                .clone()
415                .unwrap_or_else(|| cortical_id_str.clone());
416
417            let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
418                if boundaries.len() != 3 {
419                    return Err(EvoError::InvalidArea(format!(
420                        "Invalid block_boundaries for {}: expected 3 values, got {}",
421                        cortical_id_str,
422                        boundaries.len()
423                    )));
424                }
425                Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
426                    .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
427            } else {
428                // Default to 1x1x1 if not specified (should not happen in valid genomes)
429                warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
430                Dimensions::new(1, 1, 1).map_err(|e| {
431                    EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
432                })?
433            };
434
435            let position = if let Some(coords) = &raw_area.relative_coordinate {
436                if coords.len() != 3 {
437                    return Err(EvoError::InvalidArea(format!(
438                        "Invalid relative_coordinate for {}: expected 3 values, got {}",
439                        cortical_id_str,
440                        coords.len()
441                    )));
442                }
443                GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
444            } else {
445                // Default to origin if not specified
446                warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
447                GenomeCoordinate3D::new(0, 0, 0)
448            };
449
450            // Determine cortical type from cortical_id
451            let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
452                EvoError::InvalidArea(format!(
453                    "Failed to determine cortical type from ID {}: {}",
454                    cortical_id_str, e
455                ))
456            })?;
457
458            // Create cortical area with CorticalID object (zero-copy, type-safe)
459            let mut area = CorticalArea::new(
460                cortical_id,
461                0, // cortical_idx will be assigned by ConnectomeManager
462                name,
463                dimensions,
464                position,
465                cortical_type,
466            )?;
467
468            // Store cortical_type as cortical_group for new type system
469            if let Some(ref cortical_type_str) = raw_area.cortical_type {
470                area.properties.insert(
471                    "cortical_group".to_string(),
472                    serde_json::json!(cortical_type_str),
473                );
474            }
475
476            // Store all properties in the properties HashMap
477            // Neural properties
478            if let Some(v) = raw_area.synapse_attractivity {
479                area.properties
480                    .insert("synapse_attractivity".to_string(), serde_json::json!(v));
481            }
482            if let Some(v) = raw_area.refractory_period {
483                area.properties
484                    .insert("refractory_period".to_string(), serde_json::json!(v));
485            }
486            if let Some(v) = raw_area.firing_threshold {
487                area.properties
488                    .insert("firing_threshold".to_string(), serde_json::json!(v));
489            }
490            if let Some(v) = raw_area.firing_threshold_limit {
491                area.properties
492                    .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
493            }
494            if let Some(v) = raw_area.firing_threshold_increment_x {
495                area.properties.insert(
496                    "firing_threshold_increment_x".to_string(),
497                    serde_json::json!(v),
498                );
499            }
500            if let Some(v) = raw_area.firing_threshold_increment_y {
501                area.properties.insert(
502                    "firing_threshold_increment_y".to_string(),
503                    serde_json::json!(v),
504                );
505            }
506            if let Some(v) = raw_area.firing_threshold_increment_z {
507                area.properties.insert(
508                    "firing_threshold_increment_z".to_string(),
509                    serde_json::json!(v),
510                );
511            }
512            if let Some(v) = raw_area.leak_coefficient {
513                area.properties
514                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
515            }
516            if let Some(v) = raw_area.leak_variability {
517                area.properties
518                    .insert("leak_variability".to_string(), serde_json::json!(v));
519            }
520            if let Some(v) = raw_area.neuron_excitability {
521                area.properties
522                    .insert("neuron_excitability".to_string(), serde_json::json!(v));
523            }
524            if let Some(v) = raw_area.postsynaptic_current {
525                area.properties
526                    .insert("postsynaptic_current".to_string(), serde_json::json!(v));
527            }
528            if let Some(v) = raw_area.postsynaptic_current_max {
529                area.properties
530                    .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
531            }
532            if let Some(v) = raw_area.degeneration {
533                area.properties
534                    .insert("degeneration".to_string(), serde_json::json!(v));
535            }
536
537            // Boolean properties
538            if let Some(v) = raw_area.psp_uniform_distribution {
539                area.properties
540                    .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
541            }
542            if let Some(v) = raw_area.mp_charge_accumulation {
543                area.properties
544                    .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
545            }
546            if let Some(v) = raw_area.mp_driven_psp {
547                area.properties
548                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
549                tracing::info!(
550                    target: "feagi-evo",
551                    "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
552                    v,
553                    cortical_id_str
554                );
555            } else {
556                tracing::debug!(
557                    target: "feagi-evo",
558                    "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
559                    cortical_id_str
560                );
561            }
562            if let Some(v) = raw_area.visualization {
563                area.properties
564                    .insert("visualization".to_string(), serde_json::json!(v));
565                // Also store as "visible" for compatibility with getters
566                area.properties
567                    .insert("visible".to_string(), serde_json::json!(v));
568            }
569            if let Some(v) = raw_area.burst_engine_activation {
570                area.properties
571                    .insert("burst_engine_active".to_string(), serde_json::json!(v));
572            }
573            if let Some(v) = raw_area.is_mem_type {
574                area.properties
575                    .insert("is_mem_type".to_string(), serde_json::json!(v));
576            }
577
578            // Memory properties
579            if let Some(v) = raw_area.longterm_mem_threshold {
580                area.properties
581                    .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
582            }
583            if let Some(v) = raw_area.lifespan_growth_rate {
584                area.properties
585                    .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
586            }
587            if let Some(v) = raw_area.init_lifespan {
588                area.properties
589                    .insert("init_lifespan".to_string(), serde_json::json!(v));
590            }
591            if let Some(v) = raw_area.temporal_depth {
592                area.properties
593                    .insert("temporal_depth".to_string(), serde_json::json!(v));
594            }
595            if let Some(v) = raw_area.consecutive_fire_cnt_max {
596                area.properties
597                    .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
598                // Also store as "consecutive_fire_limit" for getter compatibility
599                area.properties
600                    .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
601            }
602            if let Some(v) = raw_area.snooze_length {
603                area.properties
604                    .insert("snooze_period".to_string(), serde_json::json!(v));
605            }
606
607            // Other properties
608            if let Some(v) = &raw_area.group_id {
609                area.properties
610                    .insert("group_id".to_string(), serde_json::json!(v));
611            }
612            if let Some(v) = &raw_area.sub_group_id {
613                area.properties
614                    .insert("sub_group_id".to_string(), serde_json::json!(v));
615            }
616            // Store neurons_per_voxel in properties HashMap
617            if let Some(v) = raw_area.per_voxel_neuron_cnt {
618                area.properties
619                    .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
620            }
621            if let Some(v) = &raw_area.cortical_mapping_dst {
622                // Convert dstmap keys from old format to base64
623                let converted_dstmap = convert_dstmap_keys_to_base64(v);
624                area.properties
625                    .insert("cortical_mapping_dst".to_string(), converted_dstmap);
626            }
627            if let Some(v) = &raw_area.coordinate_2d {
628                area.properties
629                    .insert("2d_coordinate".to_string(), serde_json::json!(v));
630            }
631
632            // Store any other custom properties
633            for (key, value) in &raw_area.other {
634                area.properties.insert(key.clone(), value.clone());
635            }
636
637            // Note: cortical_type parsing disabled - CorticalArea is now a minimal data structure
638            // CorticalAreaType information is stored in properties["cortical_group"] if needed
639
640            areas.push(area);
641        }
642
643        Ok(areas)
644    }
645
646    /// Parse brain regions
647    fn parse_brain_regions(
648        raw_regions: &HashMap<String, RawBrainRegion>,
649    ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
650        let mut regions = Vec::with_capacity(raw_regions.len());
651
652        for (region_id_str, raw_region) in raw_regions.iter() {
653            let title = raw_region
654                .title
655                .clone()
656                .unwrap_or_else(|| region_id_str.clone());
657
658            // Convert string region_id to RegionID (UUID)
659            // For now, try to parse as UUID if it's already a UUID, otherwise generate new one
660            let region_id = match RegionID::from_string(region_id_str) {
661                Ok(id) => id,
662                Err(_) => {
663                    // If not a valid UUID, generate a new one
664                    // This handles legacy string-based region IDs
665                    RegionID::new()
666                }
667            };
668
669            let region_type = RegionType::Undefined; // Default to Undefined
670
671            let mut region = BrainRegion::new(region_id, title, region_type)?;
672
673            // v3 RuntimeGenome sections nest IO under `properties`; merge before list fields.
674            if let Some(props) = &raw_region.properties {
675                for (k, v) in props {
676                    region.add_property(k.clone(), v.clone());
677                }
678            }
679
680            // Add cortical areas to region (using CorticalID directly)
681            if let Some(areas) = &raw_region.areas {
682                for area_id in areas {
683                    // Convert area_id to CorticalID
684                    match string_to_cortical_id(area_id) {
685                        Ok(cortical_id) => {
686                            region.add_area(cortical_id);
687                        }
688                        Err(e) => {
689                            warn!(target: "feagi-evo",
690                                "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
691                                area_id, e);
692                        }
693                    }
694                }
695            }
696
697            // Store properties in HashMap
698            if let Some(desc) = &raw_region.description {
699                region.add_property("description".to_string(), serde_json::json!(desc));
700            }
701            if let Some(coord_2d) = &raw_region.coordinate_2d {
702                region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
703            }
704            if let Some(coord_3d) = &raw_region.coordinate_3d {
705                region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
706            }
707            // Store inputs/outputs as base64 strings
708            if let Some(inputs) = &raw_region.inputs {
709                let input_ids: Vec<String> = inputs
710                    .iter()
711                    .filter_map(|id| match string_to_cortical_id(id) {
712                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
713                        Err(e) => {
714                            warn!(target: "feagi-evo",
715                                    "Failed to convert brain region input ID '{}': {}. Skipping.",
716                                    id, e);
717                            None
718                        }
719                    })
720                    .collect();
721                if !input_ids.is_empty() {
722                    region.add_property("inputs".to_string(), serde_json::json!(input_ids));
723                }
724            }
725            if let Some(outputs) = &raw_region.outputs {
726                let output_ids: Vec<String> = outputs
727                    .iter()
728                    .filter_map(|id| match string_to_cortical_id(id) {
729                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
730                        Err(e) => {
731                            warn!(target: "feagi-evo",
732                                    "Failed to convert brain region output ID '{}': {}. Skipping.",
733                                    id, e);
734                            None
735                        }
736                    })
737                    .collect();
738                if !output_ids.is_empty() {
739                    region.add_property("outputs".to_string(), serde_json::json!(output_ids));
740                }
741            }
742            if let Some(signature) = &raw_region.signature {
743                region.add_property("signature".to_string(), serde_json::json!(signature));
744            }
745
746            if let Some(d) = &raw_region.designated_inputs {
747                let ids: Vec<String> = d
748                    .iter()
749                    .filter_map(|id| match string_to_cortical_id(id) {
750                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
751                        Err(e) => {
752                            warn!(target: "feagi-evo",
753                                "Failed to convert designated_inputs entry '{}': {}. Skipping.",
754                                id, e);
755                            None
756                        }
757                    })
758                    .collect();
759                if !ids.is_empty() {
760                    region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
761                }
762            }
763            if let Some(d) = &raw_region.designated_outputs {
764                let ids: Vec<String> = d
765                    .iter()
766                    .filter_map(|id| match string_to_cortical_id(id) {
767                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
768                        Err(e) => {
769                            warn!(target: "feagi-evo",
770                                "Failed to convert designated_outputs entry '{}': {}. Skipping.",
771                                id, e);
772                            None
773                        }
774                    })
775                    .collect();
776                if !ids.is_empty() {
777                    region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
778                }
779            }
780
781            Self::normalize_brain_region_cortical_id_list_properties(
782                &mut region,
783                &[
784                    "inputs",
785                    "outputs",
786                    "designated_inputs",
787                    "designated_outputs",
788                ],
789            );
790
791            // Store parent_id for hierarchy construction
792            let parent_id = raw_region.parent_region_id.clone();
793            if let Some(ref parent_id_str) = parent_id {
794                // Store as property for serialization
795                region.add_property(
796                    "parent_region_id".to_string(),
797                    serde_json::json!(parent_id_str),
798                );
799            }
800
801            regions.push((region, parent_id));
802        }
803
804        Ok(regions)
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811
812    #[test]
813    fn test_parse_minimal_genome() {
814        // Test backward compatibility: parsing v2.1 genome with old 6-byte cortical ID
815        // Parser should convert old format to base64 for storage
816        let json = r#"{
817            "version": "2.1",
818            "blueprint": {
819                "_power": {
820                    "cortical_name": "Test Area",
821                    "block_boundaries": [10, 10, 10],
822                    "relative_coordinate": [0, 0, 0],
823                    "cortical_type": "CORE"
824                }
825            },
826            "brain_regions": {
827                "root": {
828                    "title": "Root",
829                    "parent_region_id": null,
830                    "areas": ["_power"]
831                }
832            }
833        }"#;
834
835        let parsed = GenomeParser::parse(json).unwrap();
836
837        assert_eq!(parsed.version, "2.1");
838        assert_eq!(parsed.cortical_areas.len(), 1);
839        // Input was "_power" (6 bytes), converted to "___power" (8 bytes, padded at start with underscores) then base64 encoded
840        assert_eq!(
841            parsed.cortical_areas[0].cortical_id.as_base_64(),
842            "X19fcG93ZXI="
843        );
844        assert_eq!(parsed.cortical_areas[0].name, "Test Area");
845        assert_eq!(parsed.brain_regions.len(), 1);
846
847        // Phase 2: Verify cortical_type_new is populated
848        // Note: cortical_type_new field removed - type is encoded in cortical_id
849        assert!(parsed.cortical_areas[0]
850            .cortical_id
851            .as_cortical_type()
852            .is_ok());
853    }
854
855    #[test]
856    fn test_parse_multiple_areas() {
857        // Test parsing multiple cortical areas with old format IDs
858        let json = r#"{
859            "version": "2.1",
860            "blueprint": {
861                "_power": {
862                    "cortical_name": "Area 1",
863                    "cortical_type": "CORE",
864                    "block_boundaries": [5, 5, 5],
865                    "relative_coordinate": [0, 0, 0]
866                },
867                "_death": {
868                    "cortical_name": "Area 2",
869                    "cortical_type": "CORE",
870                    "block_boundaries": [10, 10, 10],
871                    "relative_coordinate": [5, 0, 0]
872                }
873            }
874        }"#;
875
876        let parsed = GenomeParser::parse(json).unwrap();
877
878        assert_eq!(parsed.cortical_areas.len(), 2);
879
880        // Phase 2: Verify both areas have cortical_type_new populated
881        for area in &parsed.cortical_areas {
882            assert!(
883                area.cortical_id.as_cortical_type().is_ok(),
884                "Area {} should have cortical_type_new populated",
885                area.cortical_id
886            );
887        }
888    }
889
890    #[test]
891    fn test_string_to_cortical_id_legacy_power_shorthand() {
892        // Older FEAGI genomes may encode the power core area as "___pwr" (6-char shorthand).
893        // Migration must map this deterministically to the core Power cortical ID.
894        use feagi_structures::genomic::cortical_area::CoreCorticalType;
895        let id = string_to_cortical_id("___pwr").unwrap();
896        assert_eq!(
897            id.as_base_64(),
898            CoreCorticalType::Power.to_cortical_id().as_base_64()
899        );
900    }
901
902    #[test]
903    fn test_string_to_cortical_id_legacy_power_padded() {
904        // 8-char padded form ___pwr__ (from 6-char padding in legacy flat genomes).
905        use feagi_structures::genomic::cortical_area::CoreCorticalType;
906        let id = string_to_cortical_id("___pwr__").unwrap();
907        assert_eq!(
908            id.as_base_64(),
909            CoreCorticalType::Power.to_cortical_id().as_base_64()
910        );
911    }
912
913    #[test]
914    fn test_parse_with_properties() {
915        let json = r#"{
916            "version": "2.1",
917            "blueprint": {
918                "mem001": {
919                    "cortical_name": "Memory Area",
920                    "block_boundaries": [8, 8, 8],
921                    "relative_coordinate": [0, 0, 0],
922                    "cortical_type": "MEMORY",
923                    "is_mem_type": true,
924                    "firing_threshold": 50.0,
925                    "leak_coefficient": 0.9
926                }
927            }
928        }"#;
929
930        let parsed = GenomeParser::parse(json).unwrap();
931
932        assert_eq!(parsed.cortical_areas.len(), 1);
933        let area = &parsed.cortical_areas[0];
934
935        // Old type system (deprecated)
936        use feagi_structures::genomic::cortical_area::CorticalAreaType;
937        assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
938
939        // Properties stored correctly
940        assert!(area.properties.contains_key("is_mem_type"));
941        assert!(area.properties.contains_key("firing_threshold"));
942        assert!(area.properties.contains_key("cortical_group"));
943
944        // NEW: cortical_type should be derivable from cortical_id (Phase 2)
945        assert!(
946            area.cortical_id.as_cortical_type().is_ok(),
947            "cortical_id should be parseable to cortical_type"
948        );
949        if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
950            use feagi_structures::genomic::cortical_area::CorticalAreaType;
951            assert!(
952                matches!(cortical_type, CorticalAreaType::Memory(_)),
953                "Should be classified as MEMORY type"
954            );
955        }
956    }
957
958    /// v3 save embeds IO lists under `properties`; loading must preserve designated_inputs for BV presets.
959    #[test]
960    fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
961        let json = r#"{
962            "version": "3.0",
963            "blueprint": {
964                "_power": {
965                    "cortical_name": "Core",
966                    "block_boundaries": [10, 10, 10],
967                    "relative_coordinate": [0, 0, 0],
968                    "cortical_type": "CORE"
969                }
970            },
971            "brain_regions": {
972                "550e8400-e29b-41d4-a716-446655440000": {
973                    "name": "Sub",
974                    "cortical_areas": ["_power"],
975                    "properties": {
976                        "designated_inputs": ["_power"],
977                        "designated_outputs": []
978                    }
979                }
980            }
981        }"#;
982
983        let parsed = GenomeParser::parse(json).unwrap();
984        assert_eq!(parsed.brain_regions.len(), 1);
985        let (region, _) = &parsed.brain_regions[0];
986        let di = region
987            .get_property("designated_inputs")
988            .and_then(|v| v.as_array())
989            .expect("designated_inputs");
990        assert_eq!(di.len(), 1);
991        assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
992    }
993
994    #[test]
995    fn test_invalid_version() {
996        let json = r#"{
997            "version": "1.0",
998            "blueprint": {}
999        }"#;
1000
1001        let result = GenomeParser::parse(json);
1002        assert!(result.is_err());
1003    }
1004
1005    #[test]
1006    fn test_malformed_json() {
1007        let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
1008
1009        let result = GenomeParser::parse(json);
1010        assert!(result.is_err());
1011    }
1012
1013    #[test]
1014    fn test_cortical_type_new_population() {
1015        // Test that cortical_type_new field is populated during parsing (Phase 2)
1016        // This tests that parsing works with valid cortical IDs and populates types correctly
1017        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1018        let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
1019        let json = format!(
1020            r#"{{
1021            "version": "2.1",
1022            "blueprint": {{
1023                "cvision1": {{
1024                    "cortical_name": "Test Custom Vision",
1025                    "cortical_type": "CUSTOM",
1026                    "block_boundaries": [10, 10, 1],
1027                    "relative_coordinate": [0, 0, 0]
1028                }},
1029                "cmotor01": {{
1030                    "cortical_name": "Test Custom Motor",
1031                    "cortical_type": "CUSTOM",
1032                    "block_boundaries": [5, 5, 1],
1033                    "relative_coordinate": [0, 0, 0]
1034                }},
1035                "{}": {{
1036                    "cortical_name": "Test Core",
1037                    "cortical_type": "CORE",
1038                    "block_boundaries": [1, 1, 1],
1039                    "relative_coordinate": [0, 0, 0]
1040                }}
1041            }}
1042        }}"#,
1043            power_id
1044        );
1045
1046        let parsed = GenomeParser::parse(&json).unwrap();
1047        assert_eq!(parsed.cortical_areas.len(), 3);
1048
1049        // Verify all areas have cortical_type_new populated
1050        for area in &parsed.cortical_areas {
1051            assert!(
1052                area.cortical_id.as_cortical_type().is_ok(),
1053                "Area {} should have cortical_type_new populated",
1054                area.cortical_id
1055            );
1056
1057            // Verify cortical_group property is also set
1058            assert!(
1059                area.properties.contains_key("cortical_group"),
1060                "Area {} should have cortical_group property",
1061                area.cortical_id
1062            );
1063
1064            // Verify cortical group is consistent (avoid depending on feagi-brain-development)
1065            if let Some(prop_group) = area
1066                .properties
1067                .get("cortical_group")
1068                .and_then(|v| v.as_str())
1069            {
1070                assert!(
1071                    !prop_group.is_empty(),
1072                    "Area {} should have non-empty cortical_group property",
1073                    area.cortical_id.as_base_64()
1074                );
1075            }
1076        }
1077    }
1078}