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