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