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