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