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::classifiers::Classifier;
51use feagi_structures::genomic::cortical_area::CorticalID;
52use feagi_structures::genomic::cortical_area::{
53    CorticalArea, CorticalAreaDimensions as Dimensions,
54};
55use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
56use feagi_structures::genomic::{BrainRegion, RegionType};
57
58/// Parsed genome data ready for ConnectomeManager
59#[derive(Debug, Clone)]
60pub struct ParsedGenome {
61    /// Genome metadata
62    pub genome_id: String,
63    pub genome_title: String,
64    pub version: String,
65
66    /// Cortical areas extracted from blueprint
67    pub cortical_areas: Vec<CorticalArea>,
68
69    /// Brain regions and hierarchy
70    pub brain_regions: Vec<(BrainRegion, Option<String>)>, // (region, parent_id)
71
72    /// First-class classifier assemblies (parallel to brain_regions)
73    pub classifiers: Vec<Classifier>,
74
75    /// Raw neuron morphologies (for later processing)
76    pub neuron_morphologies: HashMap<String, Value>,
77
78    /// Raw modulator instances (for later processing)
79    pub modulators: HashMap<String, Value>,
80
81    /// Raw physiology data (for later processing)
82    pub physiology: Option<Value>,
83
84    /// Recorded structural changes, carried as-is (see `RuntimeGenome::change_history`)
85    pub change_history: Vec<Value>,
86}
87
88/// Raw genome JSON structure for deserialization
89#[derive(Debug, Clone, Deserialize, Serialize)]
90pub struct RawGenome {
91    pub genome_id: Option<String>,
92    pub genome_title: Option<String>,
93    pub genome_description: Option<String>,
94    pub version: String,
95    /// Integer schema version. Optional on the wire so older genomes that
96    /// pre-date this field still deserialize. The authoritative resolver
97    /// is `crate::genome::schema::detect_schema_version` and consumers
98    /// MUST go through it instead of branching on this field directly.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub genome_schema_version: Option<u32>,
101    pub blueprint: HashMap<String, RawCorticalArea>,
102    #[serde(default)]
103    pub brain_regions: HashMap<String, RawBrainRegion>,
104    #[serde(default)]
105    pub classifiers: HashMap<String, RawClassifier>,
106    #[serde(default)]
107    pub neuron_morphologies: HashMap<String, Value>,
108    #[serde(default)]
109    pub modulators: HashMap<String, Value>,
110    #[serde(default)]
111    pub physiology: Option<Value>,
112    /// Root brain region ID (UUID string) - for O(1) root lookup
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub brain_regions_root: Option<String>,
115    /// Recorded structural changes (optional; see `RuntimeGenome::change_history`)
116    #[serde(default, skip_serializing_if = "Vec::is_empty")]
117    pub change_history: Vec<Value>,
118}
119
120/// Raw cortical area from blueprint
121#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct RawCorticalArea {
123    pub cortical_name: Option<String>,
124    pub block_boundaries: Option<Vec<u32>>,
125    pub relative_coordinate: Option<Vec<i32>>,
126    pub cortical_type: Option<String>,
127
128    // Optional properties
129    pub group_id: Option<String>,
130    pub sub_group_id: Option<String>,
131    pub per_voxel_neuron_cnt: Option<u32>,
132    pub cortical_mapping_dst: Option<Value>,
133
134    // Neural properties
135    pub synapse_attractivity: Option<f32>,
136    pub refractory_period: Option<u32>,
137    pub firing_threshold: Option<f32>,
138    pub firing_threshold_limit: Option<f32>,
139    pub firing_threshold_increment_x: Option<f32>,
140    pub firing_threshold_increment_y: Option<f32>,
141    pub firing_threshold_increment_z: Option<f32>,
142    pub leak_coefficient: Option<f32>,
143    pub leak_variability: Option<f32>,
144    pub neuron_excitability: Option<f32>,
145    pub postsynaptic_current: Option<f32>,
146    pub postsynaptic_current_max: Option<f32>,
147    pub degeneration: Option<f32>,
148    pub psp_uniform_distribution: Option<bool>,
149    pub mp_charge_accumulation: Option<bool>,
150    pub mp_driven_psp: Option<bool>,
151    pub visualization: Option<bool>,
152    pub burst_engine_activation: Option<bool>,
153    #[serde(rename = "2d_coordinate")]
154    pub coordinate_2d: Option<Vec<i32>>,
155
156    // Memory properties
157    pub is_mem_type: Option<bool>,
158    pub longterm_mem_threshold: Option<u32>,
159    pub lifespan_growth_rate: Option<f32>,
160    pub init_lifespan: Option<u32>,
161    pub temporal_depth: Option<u32>,
162    pub mp_learning_enabled: Option<bool>,
163    pub mp_change_mode: Option<String>,
164    pub mp_delta_quantization: Option<f32>,
165    pub mp_ratio_quantization: Option<f32>,
166    pub min_window_activity: Option<u32>,
167    pub scan_skip_density: Option<f32>,
168    pub consecutive_fire_cnt_max: Option<u32>,
169    pub snooze_length: Option<u32>,
170
171    // Allow any other properties (future-proofing)
172    #[serde(flatten)]
173    pub other: HashMap<String, Value>,
174}
175
176/// Raw brain region from genome
177#[derive(Debug, Clone, Deserialize, Serialize)]
178pub struct RawBrainRegion {
179    #[serde(alias = "name")]
180    pub title: Option<String>,
181    pub description: Option<String>,
182    pub parent_region_id: Option<String>,
183    pub coordinate_2d: Option<Vec<i32>>,
184    pub coordinate_3d: Option<Vec<i32>>,
185    #[serde(alias = "cortical_areas")]
186    pub areas: Option<Vec<String>>,
187    pub regions: Option<Vec<String>>,
188    pub inputs: Option<Vec<String>>,
189    pub outputs: Option<Vec<String>>,
190    /// Declared interface lists (persisted from RuntimeGenome / PUT region).
191    pub designated_inputs: Option<Vec<String>>,
192    pub designated_outputs: Option<Vec<String>>,
193    pub signature: Option<String>,
194    /// v3 `serde_json::to_value(BrainRegion)` nests `inputs` / `designated_*` under `properties`.
195    pub properties: Option<HashMap<String, Value>>,
196}
197
198/// Raw classifier assembly from the top-level `classifiers` genome key.
199#[derive(Debug, Clone, Deserialize, Serialize)]
200pub struct RawClassifier {
201    #[serde(alias = "title")]
202    pub name: Option<String>,
203    pub parent_region_id: Option<String>,
204    #[serde(alias = "coordinate_3d")]
205    pub coordinates_3d: Option<Vec<i32>>,
206    pub kernel_area_id: Option<String>,
207    pub class_area_id: Option<String>,
208    #[serde(default)]
209    pub training_mode: Option<feagi_structures::genomic::classifiers::ClassifierTrainingMode>,
210    pub mask_area_id: Option<String>,
211    pub kernel_size: Option<[u32; 3]>,
212    /// Scanner-mode class count. Required in scanner mode.
213    pub class_count: Option<u32>,
214    /// Current field bindings. Each entry is one Classifier mapping and its twin.
215    pub fields: Option<Vec<feagi_structures::genomic::classifiers::ClassifierField>>,
216    /// Previous singular field record. Loaded as one binding when `fields` is absent.
217    pub field_area_id: Option<String>,
218    pub kernel_memory_id: Option<String>,
219    pub class_memory_id: Option<String>,
220    #[serde(default)]
221    pub reward_training: bool,
222    #[serde(default)]
223    pub answer_feedback_area_id: Option<String>,
224    #[serde(default)]
225    pub pain_area_id: Option<String>,
226    #[serde(default)]
227    pub pleasure_area_id: Option<String>,
228    #[serde(default)]
229    pub answer_latency_bursts: u32,
230    #[serde(default)]
231    pub learn_area_id: Option<String>,
232    #[serde(default)]
233    pub confidence_area_id: Option<String>,
234    /// Previous singular twin record. Paired with `field_area_id` on load.
235    pub scan_twin_id: Option<String>,
236    pub properties: Option<HashMap<String, Value>>,
237}
238
239fn classifier_fields_from_raw(
240    raw: &RawClassifier,
241) -> Vec<feagi_structures::genomic::classifiers::ClassifierField> {
242    if let Some(fields) = &raw.fields {
243        return fields
244            .iter()
245            .filter(|field| !field.field_area_id.is_empty() && !field.scan_twin_id.is_empty())
246            .cloned()
247            .collect();
248    }
249    match (&raw.field_area_id, &raw.scan_twin_id) {
250        (Some(field_area_id), Some(scan_twin_id))
251            if !field_area_id.is_empty() && !scan_twin_id.is_empty() =>
252        {
253            vec![feagi_structures::genomic::classifiers::ClassifierField {
254                field_area_id: field_area_id.clone(),
255                scan_twin_id: scan_twin_id.clone(),
256            }]
257        }
258        _ => Vec::new(),
259    }
260}
261
262/// Convert cortical_mapping_dst keys from old format to base64
263///
264/// This ensures all destination cortical IDs in dstmap are stored in the new base64 format.
265fn convert_dstmap_keys_to_base64(dstmap: &Value) -> Value {
266    if let Some(dstmap_obj) = dstmap.as_object() {
267        let mut converted = serde_json::Map::new();
268
269        for (dest_id_str, mapping_value) in dstmap_obj {
270            // Convert destination cortical_id to base64 format
271            match string_to_cortical_id(dest_id_str) {
272                Ok(dest_cortical_id) => {
273                    converted.insert(dest_cortical_id.as_base_64(), mapping_value.clone());
274                }
275                Err(e) => {
276                    // If conversion fails, keep original and log warning
277                    tracing::warn!(
278                        "Failed to convert dstmap key '{}' to base64: {}, keeping original",
279                        dest_id_str,
280                        e
281                    );
282                    converted.insert(dest_id_str.clone(), mapping_value.clone());
283                }
284            }
285        }
286
287        Value::Object(converted)
288    } else {
289        // Not an object, return as-is
290        dstmap.clone()
291    }
292}
293
294/// Convert a string cortical_id to CorticalID
295/// Handles both old 6-char format and new base64 format
296/// CRITICAL: Uses feagi-data-processing types as single source of truth for core areas
297pub fn string_to_cortical_id(id_str: &str) -> EvoResult<CorticalID> {
298    use feagi_structures::genomic::cortical_area::CoreCorticalType;
299
300    // Try base64 first (new format)
301    if let Ok(cortical_id) = CorticalID::try_from_base_64(id_str) {
302        let mut bytes = [0u8; CorticalID::CORTICAL_ID_LENGTH];
303        cortical_id.write_id_to_bytes(&mut bytes);
304        if bytes == *b"___power" {
305            return Ok(CoreCorticalType::Power.to_cortical_id());
306        }
307        if bytes == *b"___death" {
308            return Ok(CoreCorticalType::Death.to_cortical_id());
309        }
310        if bytes == *b"___fatig" {
311            return Ok(CoreCorticalType::Fatigue.to_cortical_id());
312        }
313        if bytes == *b"___pain_" {
314            return Ok(CoreCorticalType::Pain.to_cortical_id());
315        }
316        if bytes == *b"___pleas" {
317            return Ok(CoreCorticalType::Pleasure.to_cortical_id());
318        }
319        if bytes == *b"___fear_" {
320            return Ok(CoreCorticalType::Fear.to_cortical_id());
321        }
322        if bytes == *b"___hope_" {
323            return Ok(CoreCorticalType::Hope.to_cortical_id());
324        }
325        return Ok(cortical_id);
326    }
327
328    // Handle legacy CORE area names (6-char format) - use proper types from feagi-data-processing
329    if id_str == "_power" {
330        return Ok(CoreCorticalType::Power.to_cortical_id());
331    }
332    // Legacy shorthand used by older FEAGI genomes: "___pwr" (6-char) refers to core Power.
333    if id_str == "___pwr" {
334        return Ok(CoreCorticalType::Power.to_cortical_id());
335    }
336    // Legacy 8-char core names used in some BV caches
337    if id_str == "___power" {
338        return Ok(CoreCorticalType::Power.to_cortical_id());
339    }
340    // 8-char padded form of ___pwr (from 6-char padding in legacy flat genomes)
341    if id_str == "___pwr__" {
342        return Ok(CoreCorticalType::Power.to_cortical_id());
343    }
344    if id_str == "___death" {
345        return Ok(CoreCorticalType::Death.to_cortical_id());
346    }
347    if id_str == "___fatig" {
348        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
349    }
350    if id_str == "___pain_" {
351        return Ok(CoreCorticalType::Pain.to_cortical_id());
352    }
353    if id_str == "___pleas" {
354        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
355    }
356    if id_str == "___fear_" {
357        return Ok(CoreCorticalType::Fear.to_cortical_id());
358    }
359    if id_str == "___hope_" {
360        return Ok(CoreCorticalType::Hope.to_cortical_id());
361    }
362    if id_str == "_death" {
363        return Ok(CoreCorticalType::Death.to_cortical_id());
364    }
365    if id_str == "_fatigue" {
366        return Ok(CoreCorticalType::Fatigue.to_cortical_id());
367    }
368    if id_str == "_pain" {
369        return Ok(CoreCorticalType::Pain.to_cortical_id());
370    }
371    if id_str == "_pleasure" {
372        return Ok(CoreCorticalType::Pleasure.to_cortical_id());
373    }
374    if id_str == "_fear" {
375        return Ok(CoreCorticalType::Fear.to_cortical_id());
376    }
377    if id_str == "_hope" {
378        return Ok(CoreCorticalType::Hope.to_cortical_id());
379    }
380
381    // For non-core areas, use CorticalID's legacy ASCII parser (6-char and 8-char)
382    if id_str.len() == 6 || id_str.len() == 8 {
383        CorticalID::try_from_legacy_ascii(id_str).map_err(|e| {
384            EvoError::InvalidArea(format!("Failed to convert cortical_id '{}': {}", id_str, e))
385        })
386    } else {
387        Err(EvoError::InvalidArea(format!(
388            "Invalid cortical_id length: '{}' (expected 6 or 8 ASCII chars, or base64)",
389            id_str
390        )))
391    }
392}
393
394/// Genome parser
395pub struct GenomeParser;
396
397/// Kernel/class/mask area bindings, kernel size, and class count after training-mode normalization.
398type NormalizedClassifierTraining = (
399    Option<String>,
400    Option<String>,
401    Option<String>,
402    Option<[u32; 3]>,
403    Option<u32>,
404);
405
406impl GenomeParser {
407    /// Normalize cortical ID list properties (inputs, outputs, designated_*) to base64 strings.
408    fn normalize_brain_region_cortical_id_list_properties(region: &mut BrainRegion, keys: &[&str]) {
409        for key in keys {
410            let Some(val) = region.get_property(key) else {
411                continue;
412            };
413            let Some(arr) = val.as_array() else {
414                continue;
415            };
416            let mut out: Vec<String> = Vec::new();
417            for item in arr {
418                let Some(s) = item.as_str() else {
419                    continue;
420                };
421                match string_to_cortical_id(s) {
422                    Ok(cortical_id) => out.push(cortical_id.as_base_64()),
423                    Err(e) => {
424                        warn!(target: "feagi-evo",
425                            "Failed to convert brain region '{}' entry '{}': {}. Skipping.",
426                            key, s, e);
427                    }
428                }
429            }
430            if out.is_empty() {
431                region.properties.remove(*key);
432            } else {
433                region.add_property((*key).to_string(), serde_json::json!(out));
434            }
435        }
436    }
437
438    /// Parse a genome JSON string into a ParsedGenome
439    ///
440    /// # Arguments
441    ///
442    /// * `json_str` - JSON string of the genome
443    ///
444    /// # Returns
445    ///
446    /// Parsed genome ready for loading into ConnectomeManager
447    ///
448    /// # Errors
449    ///
450    /// Returns error if:
451    /// - JSON is malformed
452    /// - Required fields are missing
453    /// - Data types are invalid
454    ///
455    pub fn parse(json_str: &str) -> EvoResult<ParsedGenome> {
456        // Deserialize raw genome
457        let raw: RawGenome = serde_json::from_str(json_str)
458            .map_err(|e| EvoError::InvalidGenome(format!("Failed to parse JSON: {}", e)))?;
459
460        // Validate version - support 2.x and 3.x (3.0 is flat format with base64 IDs)
461        if !raw.version.starts_with("2.") && !raw.version.starts_with("3.") && raw.version != "3" {
462            return Err(EvoError::InvalidGenome(format!(
463                "Unsupported genome version: {}. Expected 2.x or 3.x",
464                raw.version
465            )));
466        }
467
468        // Parse cortical areas from blueprint
469        let cortical_areas = Self::parse_cortical_areas(&raw.blueprint)?;
470
471        // Parse brain regions
472        let brain_regions = Self::parse_brain_regions(&raw.brain_regions)?;
473        let classifiers = Self::parse_classifiers(&raw.classifiers)?;
474
475        Ok(ParsedGenome {
476            genome_id: raw.genome_id.unwrap_or_else(|| "unknown".to_string()),
477            genome_title: raw.genome_title.unwrap_or_else(|| "Untitled".to_string()),
478            version: raw.version,
479            cortical_areas,
480            brain_regions,
481            classifiers,
482            neuron_morphologies: raw.neuron_morphologies,
483            modulators: raw.modulators,
484            physiology: raw.physiology,
485            change_history: raw.change_history,
486        })
487    }
488
489    /// Parse cortical areas from blueprint
490    fn parse_cortical_areas(
491        blueprint: &HashMap<String, RawCorticalArea>,
492    ) -> EvoResult<Vec<CorticalArea>> {
493        let mut areas = Vec::with_capacity(blueprint.len());
494
495        for (cortical_id_str, raw_area) in blueprint.iter() {
496            // Skip empty IDs
497            if cortical_id_str.is_empty() {
498                warn!(target: "feagi-evo","Skipping empty cortical_id");
499                continue;
500            }
501
502            // Convert string cortical_id to CorticalID (handles 6-char legacy and base64)
503            let cortical_id = match string_to_cortical_id(cortical_id_str) {
504                Ok(id) => id,
505                Err(e) => {
506                    warn!(target: "feagi-evo","Skipping invalid cortical_id '{}': {}", cortical_id_str, e);
507                    continue;
508                }
509            };
510
511            // Extract required fields
512            let name = raw_area
513                .cortical_name
514                .clone()
515                .unwrap_or_else(|| cortical_id_str.clone());
516
517            let dimensions = if let Some(boundaries) = &raw_area.block_boundaries {
518                if boundaries.len() != 3 {
519                    return Err(EvoError::InvalidArea(format!(
520                        "Invalid block_boundaries for {}: expected 3 values, got {}",
521                        cortical_id_str,
522                        boundaries.len()
523                    )));
524                }
525                Dimensions::new(boundaries[0], boundaries[1], boundaries[2])
526                    .map_err(|e| EvoError::InvalidArea(format!("Invalid dimensions: {}", e)))?
527            } else {
528                // Default to 1x1x1 if not specified (should not happen in valid genomes)
529                warn!(target: "feagi-evo","Cortical area {} missing block_boundaries, defaulting to 1x1x1", cortical_id_str);
530                Dimensions::new(1, 1, 1).map_err(|e| {
531                    EvoError::InvalidArea(format!("Invalid default dimensions: {}", e))
532                })?
533            };
534
535            let position = if let Some(coords) = &raw_area.relative_coordinate {
536                if coords.len() != 3 {
537                    return Err(EvoError::InvalidArea(format!(
538                        "Invalid relative_coordinate for {}: expected 3 values, got {}",
539                        cortical_id_str,
540                        coords.len()
541                    )));
542                }
543                GenomeCoordinate3D::new(coords[0], coords[1], coords[2])
544            } else {
545                // Default to origin if not specified
546                warn!(target: "feagi-evo","Cortical area {} missing relative_coordinate, defaulting to (0,0,0)", cortical_id_str);
547                GenomeCoordinate3D::new(0, 0, 0)
548            };
549
550            // Determine cortical type from cortical_id
551            let cortical_type = cortical_id.as_cortical_type().map_err(|e| {
552                EvoError::InvalidArea(format!(
553                    "Failed to determine cortical type from ID {}: {}",
554                    cortical_id_str, e
555                ))
556            })?;
557
558            // Create cortical area with CorticalID object (zero-copy, type-safe)
559            let mut area = CorticalArea::new(
560                cortical_id,
561                0, // cortical_idx will be assigned by ConnectomeManager
562                name,
563                dimensions,
564                position,
565                cortical_type,
566            )?;
567
568            // Store cortical_type as cortical_group for new type system
569            if let Some(ref cortical_type_str) = raw_area.cortical_type {
570                area.properties.insert(
571                    "cortical_group".to_string(),
572                    serde_json::json!(cortical_type_str),
573                );
574            }
575
576            // Store all properties in the properties HashMap
577            // Neural properties
578            if let Some(v) = raw_area.synapse_attractivity {
579                area.properties
580                    .insert("synapse_attractivity".to_string(), serde_json::json!(v));
581            }
582            if let Some(v) = raw_area.refractory_period {
583                area.properties
584                    .insert("refractory_period".to_string(), serde_json::json!(v));
585            }
586            if let Some(v) = raw_area.firing_threshold {
587                area.properties
588                    .insert("firing_threshold".to_string(), serde_json::json!(v));
589            }
590            if let Some(v) = raw_area.firing_threshold_limit {
591                area.properties
592                    .insert("firing_threshold_limit".to_string(), serde_json::json!(v));
593            }
594            if let Some(v) = raw_area.firing_threshold_increment_x {
595                area.properties.insert(
596                    "firing_threshold_increment_x".to_string(),
597                    serde_json::json!(v),
598                );
599            }
600            if let Some(v) = raw_area.firing_threshold_increment_y {
601                area.properties.insert(
602                    "firing_threshold_increment_y".to_string(),
603                    serde_json::json!(v),
604                );
605            }
606            if let Some(v) = raw_area.firing_threshold_increment_z {
607                area.properties.insert(
608                    "firing_threshold_increment_z".to_string(),
609                    serde_json::json!(v),
610                );
611            }
612            if let Some(v) = raw_area.leak_coefficient {
613                area.properties
614                    .insert("leak_coefficient".to_string(), serde_json::json!(v));
615            }
616            if let Some(v) = raw_area.leak_variability {
617                area.properties
618                    .insert("leak_variability".to_string(), serde_json::json!(v));
619            }
620            if let Some(v) = raw_area.neuron_excitability {
621                area.properties
622                    .insert("neuron_excitability".to_string(), serde_json::json!(v));
623            }
624            if let Some(v) = raw_area.postsynaptic_current {
625                area.properties
626                    .insert("postsynaptic_current".to_string(), serde_json::json!(v));
627            }
628            if let Some(v) = raw_area.postsynaptic_current_max {
629                area.properties
630                    .insert("postsynaptic_current_max".to_string(), serde_json::json!(v));
631            }
632            if let Some(v) = raw_area.degeneration {
633                area.properties
634                    .insert("degeneration".to_string(), serde_json::json!(v));
635            }
636
637            // Boolean properties
638            if let Some(v) = raw_area.psp_uniform_distribution {
639                area.properties
640                    .insert("psp_uniform_distribution".to_string(), serde_json::json!(v));
641            }
642            if let Some(v) = raw_area.mp_charge_accumulation {
643                area.properties
644                    .insert("mp_charge_accumulation".to_string(), serde_json::json!(v));
645            }
646            if let Some(v) = raw_area.mp_driven_psp {
647                area.properties
648                    .insert("mp_driven_psp".to_string(), serde_json::json!(v));
649                tracing::info!(
650                    target: "feagi-evo",
651                    "[GENOME-LOAD] Loaded mp_driven_psp={} for area {}",
652                    v,
653                    cortical_id_str
654                );
655            } else {
656                tracing::debug!(
657                    target: "feagi-evo",
658                    "[GENOME-LOAD] mp_driven_psp not found in raw_area for {}, will use default=false",
659                    cortical_id_str
660                );
661            }
662            if let Some(v) = raw_area.visualization {
663                area.properties
664                    .insert("visualization".to_string(), serde_json::json!(v));
665                // Also store as "visible" for compatibility with getters
666                area.properties
667                    .insert("visible".to_string(), serde_json::json!(v));
668            }
669            if let Some(v) = raw_area.burst_engine_activation {
670                area.properties
671                    .insert("burst_engine_active".to_string(), serde_json::json!(v));
672            }
673            if let Some(v) = raw_area.is_mem_type {
674                area.properties
675                    .insert("is_mem_type".to_string(), serde_json::json!(v));
676            }
677
678            // Memory properties
679            if let Some(v) = raw_area.longterm_mem_threshold {
680                area.properties
681                    .insert("longterm_mem_threshold".to_string(), serde_json::json!(v));
682            }
683            if let Some(v) = raw_area.lifespan_growth_rate {
684                area.properties
685                    .insert("lifespan_growth_rate".to_string(), serde_json::json!(v));
686            }
687            if let Some(v) = raw_area.init_lifespan {
688                area.properties
689                    .insert("init_lifespan".to_string(), serde_json::json!(v));
690            }
691            if let Some(v) = raw_area.temporal_depth {
692                area.properties
693                    .insert("temporal_depth".to_string(), serde_json::json!(v));
694            }
695            if let Some(v) = raw_area.mp_learning_enabled {
696                area.properties
697                    .insert("mp_learning_enabled".to_string(), serde_json::json!(v));
698            }
699            if let Some(v) = &raw_area.mp_change_mode {
700                area.properties
701                    .insert("mp_change_mode".to_string(), serde_json::json!(v));
702            }
703            if let Some(v) = raw_area.mp_delta_quantization {
704                area.properties
705                    .insert("mp_delta_quantization".to_string(), serde_json::json!(v));
706            }
707            if let Some(v) = raw_area.mp_ratio_quantization {
708                area.properties
709                    .insert("mp_ratio_quantization".to_string(), serde_json::json!(v));
710            }
711            if let Some(v) = raw_area.min_window_activity {
712                area.properties
713                    .insert("min_window_activity".to_string(), serde_json::json!(v));
714            }
715            if let Some(v) = raw_area.scan_skip_density {
716                area.properties
717                    .insert("scan_skip_density".to_string(), serde_json::json!(v));
718            }
719            if let Some(v) = raw_area.consecutive_fire_cnt_max {
720                area.properties
721                    .insert("consecutive_fire_cnt_max".to_string(), serde_json::json!(v));
722                // Also store as "consecutive_fire_limit" for getter compatibility
723                area.properties
724                    .insert("consecutive_fire_limit".to_string(), serde_json::json!(v));
725            }
726            if let Some(v) = raw_area.snooze_length {
727                area.properties
728                    .insert("snooze_period".to_string(), serde_json::json!(v));
729            }
730
731            // Other properties
732            if let Some(v) = &raw_area.group_id {
733                area.properties
734                    .insert("group_id".to_string(), serde_json::json!(v));
735            }
736            if let Some(v) = &raw_area.sub_group_id {
737                area.properties
738                    .insert("sub_group_id".to_string(), serde_json::json!(v));
739            }
740            // Store neurons_per_voxel in properties HashMap
741            if let Some(v) = raw_area.per_voxel_neuron_cnt {
742                area.properties
743                    .insert("neurons_per_voxel".to_string(), serde_json::json!(v));
744            }
745            if let Some(v) = &raw_area.cortical_mapping_dst {
746                // Convert dstmap keys from old format to base64
747                let converted_dstmap = convert_dstmap_keys_to_base64(v);
748                area.properties
749                    .insert("cortical_mapping_dst".to_string(), converted_dstmap);
750            }
751            if let Some(v) = &raw_area.coordinate_2d {
752                area.properties
753                    .insert("2d_coordinate".to_string(), serde_json::json!(v));
754            }
755
756            // Store any other custom properties
757            for (key, value) in &raw_area.other {
758                area.properties.insert(key.clone(), value.clone());
759            }
760
761            // Note: cortical_type parsing disabled - CorticalArea is now a minimal data structure
762            // CorticalAreaType information is stored in properties["cortical_group"] if needed
763
764            areas.push(area);
765        }
766
767        Ok(areas)
768    }
769
770    /// Parse brain regions
771    fn parse_brain_regions(
772        raw_regions: &HashMap<String, RawBrainRegion>,
773    ) -> EvoResult<Vec<(BrainRegion, Option<String>)>> {
774        let mut regions = Vec::with_capacity(raw_regions.len());
775
776        for (region_id_str, raw_region) in raw_regions.iter() {
777            let title = raw_region
778                .title
779                .clone()
780                .unwrap_or_else(|| region_id_str.clone());
781
782            // Convert string region_id to RegionID (UUID)
783            // For now, try to parse as UUID if it's already a UUID, otherwise generate new one
784            let region_id = match RegionID::from_string(region_id_str) {
785                Ok(id) => id,
786                Err(_) => {
787                    // If not a valid UUID, generate a new one
788                    // This handles legacy string-based region IDs
789                    RegionID::new()
790                }
791            };
792
793            let region_type = RegionType::Undefined; // Default to Undefined
794
795            let mut region = BrainRegion::new(region_id, title, region_type)?;
796
797            // v3 RuntimeGenome sections nest IO under `properties`; merge before list fields.
798            if let Some(props) = &raw_region.properties {
799                for (k, v) in props {
800                    region.add_property(k.clone(), v.clone());
801                }
802            }
803
804            // Add cortical areas to region (using CorticalID directly)
805            if let Some(areas) = &raw_region.areas {
806                for area_id in areas {
807                    // Convert area_id to CorticalID
808                    match string_to_cortical_id(area_id) {
809                        Ok(cortical_id) => {
810                            region.add_area(cortical_id);
811                        }
812                        Err(e) => {
813                            warn!(target: "feagi-evo",
814                                "Failed to convert brain region area ID '{}' to CorticalID: {}. Skipping.",
815                                area_id, e);
816                        }
817                    }
818                }
819            }
820
821            // Store properties in HashMap
822            if let Some(desc) = &raw_region.description {
823                region.add_property("description".to_string(), serde_json::json!(desc));
824            }
825            if let Some(coord_2d) = &raw_region.coordinate_2d {
826                region.add_property("coordinate_2d".to_string(), serde_json::json!(coord_2d));
827            }
828            if let Some(coord_3d) = &raw_region.coordinate_3d {
829                region.add_property("coordinate_3d".to_string(), serde_json::json!(coord_3d));
830            }
831            // Store inputs/outputs as base64 strings
832            if let Some(inputs) = &raw_region.inputs {
833                let input_ids: Vec<String> = inputs
834                    .iter()
835                    .filter_map(|id| match string_to_cortical_id(id) {
836                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
837                        Err(e) => {
838                            warn!(target: "feagi-evo",
839                                    "Failed to convert brain region input ID '{}': {}. Skipping.",
840                                    id, e);
841                            None
842                        }
843                    })
844                    .collect();
845                if !input_ids.is_empty() {
846                    region.add_property("inputs".to_string(), serde_json::json!(input_ids));
847                }
848            }
849            if let Some(outputs) = &raw_region.outputs {
850                let output_ids: Vec<String> = outputs
851                    .iter()
852                    .filter_map(|id| match string_to_cortical_id(id) {
853                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
854                        Err(e) => {
855                            warn!(target: "feagi-evo",
856                                    "Failed to convert brain region output ID '{}': {}. Skipping.",
857                                    id, e);
858                            None
859                        }
860                    })
861                    .collect();
862                if !output_ids.is_empty() {
863                    region.add_property("outputs".to_string(), serde_json::json!(output_ids));
864                }
865            }
866            if let Some(signature) = &raw_region.signature {
867                region.add_property("signature".to_string(), serde_json::json!(signature));
868            }
869
870            if let Some(d) = &raw_region.designated_inputs {
871                let ids: Vec<String> = d
872                    .iter()
873                    .filter_map(|id| match string_to_cortical_id(id) {
874                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
875                        Err(e) => {
876                            warn!(target: "feagi-evo",
877                                "Failed to convert designated_inputs entry '{}': {}. Skipping.",
878                                id, e);
879                            None
880                        }
881                    })
882                    .collect();
883                if !ids.is_empty() {
884                    region.add_property("designated_inputs".to_string(), serde_json::json!(ids));
885                }
886            }
887            if let Some(d) = &raw_region.designated_outputs {
888                let ids: Vec<String> = d
889                    .iter()
890                    .filter_map(|id| match string_to_cortical_id(id) {
891                        Ok(cortical_id) => Some(cortical_id.as_base_64()),
892                        Err(e) => {
893                            warn!(target: "feagi-evo",
894                                "Failed to convert designated_outputs entry '{}': {}. Skipping.",
895                                id, e);
896                            None
897                        }
898                    })
899                    .collect();
900                if !ids.is_empty() {
901                    region.add_property("designated_outputs".to_string(), serde_json::json!(ids));
902                }
903            }
904
905            Self::normalize_brain_region_cortical_id_list_properties(
906                &mut region,
907                &[
908                    "inputs",
909                    "outputs",
910                    "designated_inputs",
911                    "designated_outputs",
912                ],
913            );
914
915            // Store parent_id for hierarchy construction
916            let parent_id = raw_region.parent_region_id.clone();
917            if let Some(ref parent_id_str) = parent_id {
918                // Store as property for serialization
919                region.add_property(
920                    "parent_region_id".to_string(),
921                    serde_json::json!(parent_id_str),
922                );
923            }
924
925            regions.push((region, parent_id));
926        }
927
928        Ok(regions)
929    }
930
931    fn normalize_classifier_training(
932        classifier_id: &str,
933        training_mode: feagi_structures::genomic::classifiers::ClassifierTrainingMode,
934        kernel_area_id: Option<String>,
935        class_area_id: Option<String>,
936        mask_area_id: Option<String>,
937        kernel_size: Option<[u32; 3]>,
938        class_count: Option<u32>,
939    ) -> EvoResult<NormalizedClassifierTraining> {
940        use feagi_structures::genomic::classifiers::ClassifierTrainingMode;
941        match training_mode {
942            ClassifierTrainingMode::Kernel => {
943                if mask_area_id.is_some() || kernel_size.is_some() || class_count.is_some() {
944                    return Err(EvoError::InvalidArea(format!(
945                    "Classifier '{classifier_id}' is in kernel mode and cannot store a mask, kernel size, or class count"
946                )));
947                }
948                Ok((kernel_area_id, class_area_id, None, None, None))
949            }
950            ClassifierTrainingMode::Scanner => {
951                if kernel_area_id.is_some() || class_area_id.is_some() {
952                    return Err(EvoError::InvalidArea(format!(
953                    "Classifier '{classifier_id}' is in scanner mode and cannot store kernel or class areas"
954                )));
955                }
956                let mask = mask_area_id
957                    .filter(|id| !id.trim().is_empty())
958                    .ok_or_else(|| {
959                        EvoError::InvalidArea(format!(
960                    "Classifier '{classifier_id}' is in scanner mode and is missing mask_area_id"
961                ))
962                    })?;
963                let size = kernel_size.ok_or_else(|| {
964                    EvoError::InvalidArea(format!(
965                    "Classifier '{classifier_id}' is in scanner mode and is missing kernel_size"
966                ))
967                })?;
968                feagi_structures::genomic::classifiers::validate_kernel_size(size).map_err(
969                    |e| EvoError::InvalidArea(format!("Classifier '{classifier_id}' {e}")),
970                )?;
971                let count = class_count.ok_or_else(|| {
972                    EvoError::InvalidArea(format!(
973                    "Classifier '{classifier_id}' is in scanner mode and is missing class_count"
974                ))
975                })?;
976                feagi_structures::neuron_voxels::class_potential::validate_class_count(count)
977                    .map_err(|e| {
978                        EvoError::InvalidArea(format!("Classifier '{classifier_id}' {e}"))
979                    })?;
980                Ok((None, None, Some(mask), Some(size), Some(count)))
981            }
982        }
983    }
984
985    fn parse_classifiers(
986        raw_classifiers: &HashMap<String, RawClassifier>,
987    ) -> EvoResult<Vec<Classifier>> {
988        let mut classifiers = Vec::with_capacity(raw_classifiers.len());
989        for (classifier_id, raw) in raw_classifiers {
990            let name = raw
991                .name
992                .clone()
993                .filter(|n| !n.trim().is_empty())
994                .ok_or_else(|| {
995                    EvoError::InvalidArea(format!("Classifier '{}' is missing name", classifier_id))
996                })?;
997            let parent_region_id = raw
998                .parent_region_id
999                .clone()
1000                .filter(|n| !n.trim().is_empty())
1001                .ok_or_else(|| {
1002                    EvoError::InvalidArea(format!(
1003                        "Classifier '{}' is missing parent_region_id",
1004                        classifier_id
1005                    ))
1006                })?;
1007            let coordinates_3d = match &raw.coordinates_3d {
1008                Some(coords) if coords.len() == 3 => [coords[0], coords[1], coords[2]],
1009                Some(coords) => {
1010                    return Err(EvoError::InvalidArea(format!(
1011                        "Classifier '{}' coordinates_3d must have 3 values, got {}",
1012                        classifier_id,
1013                        coords.len()
1014                    )))
1015                }
1016                None => [0, 0, 0],
1017            };
1018            let kernel_memory_id = raw.kernel_memory_id.clone().ok_or_else(|| {
1019                EvoError::InvalidArea(format!(
1020                    "Classifier '{}' is missing kernel_memory_id",
1021                    classifier_id
1022                ))
1023            })?;
1024            let class_memory_id = raw.class_memory_id.clone().ok_or_else(|| {
1025                EvoError::InvalidArea(format!(
1026                    "Classifier '{}' is missing class_memory_id",
1027                    classifier_id
1028                ))
1029            })?;
1030            let fields = classifier_fields_from_raw(raw);
1031            let training_mode = raw.training_mode.unwrap_or_default();
1032            let (kernel_area_id, class_area_id, mask_area_id, kernel_size, class_count) =
1033                Self::normalize_classifier_training(
1034                    classifier_id,
1035                    training_mode,
1036                    raw.kernel_area_id.clone(),
1037                    raw.class_area_id.clone(),
1038                    raw.mask_area_id.clone(),
1039                    raw.kernel_size,
1040                    raw.class_count,
1041                )?;
1042            classifiers.push(Classifier {
1043                classifier_id: classifier_id.clone(),
1044                name,
1045                parent_region_id,
1046                coordinates_3d,
1047                training_mode,
1048                kernel_area_id,
1049                class_area_id,
1050                mask_area_id,
1051                class_count,
1052                kernel_size,
1053                fields,
1054                kernel_memory_id,
1055                class_memory_id,
1056                reward_training: raw.reward_training,
1057                answer_feedback_area_id: raw.answer_feedback_area_id.clone(),
1058                pain_area_id: raw.pain_area_id.clone(),
1059                pleasure_area_id: raw.pleasure_area_id.clone(),
1060                answer_latency_bursts: raw.answer_latency_bursts,
1061                learn_area_id: raw.learn_area_id.clone(),
1062                confidence_area_id: raw.confidence_area_id.clone(),
1063                properties: raw.properties.clone().unwrap_or_default(),
1064            });
1065        }
1066        Ok(classifiers)
1067    }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073
1074    #[test]
1075    fn test_parse_minimal_genome() {
1076        // Test backward compatibility: parsing v2.1 genome with old 6-byte cortical ID
1077        // Parser should convert old format to base64 for storage
1078        let json = r#"{
1079            "version": "2.1",
1080            "blueprint": {
1081                "_power": {
1082                    "cortical_name": "Test Area",
1083                    "block_boundaries": [10, 10, 10],
1084                    "relative_coordinate": [0, 0, 0],
1085                    "cortical_type": "CORE"
1086                }
1087            },
1088            "brain_regions": {
1089                "root": {
1090                    "title": "Root",
1091                    "parent_region_id": null,
1092                    "areas": ["_power"]
1093                }
1094            }
1095        }"#;
1096
1097        let parsed = GenomeParser::parse(json).unwrap();
1098
1099        assert_eq!(parsed.version, "2.1");
1100        assert_eq!(parsed.cortical_areas.len(), 1);
1101        // Input was "_power" (6 bytes), converted to "___power" (8 bytes, padded at start with underscores) then base64 encoded
1102        assert_eq!(
1103            parsed.cortical_areas[0].cortical_id.as_base_64(),
1104            "X19fcG93ZXI="
1105        );
1106        assert_eq!(parsed.cortical_areas[0].name, "Test Area");
1107        assert_eq!(parsed.brain_regions.len(), 1);
1108
1109        // Phase 2: Verify cortical_type_new is populated
1110        // Note: cortical_type_new field removed - type is encoded in cortical_id
1111        assert!(parsed.cortical_areas[0]
1112            .cortical_id
1113            .as_cortical_type()
1114            .is_ok());
1115    }
1116
1117    #[test]
1118    fn test_parse_multiple_areas() {
1119        // Test parsing multiple cortical areas with old format IDs
1120        let json = r#"{
1121            "version": "2.1",
1122            "blueprint": {
1123                "_power": {
1124                    "cortical_name": "Area 1",
1125                    "cortical_type": "CORE",
1126                    "block_boundaries": [5, 5, 5],
1127                    "relative_coordinate": [0, 0, 0]
1128                },
1129                "_death": {
1130                    "cortical_name": "Area 2",
1131                    "cortical_type": "CORE",
1132                    "block_boundaries": [10, 10, 10],
1133                    "relative_coordinate": [5, 0, 0]
1134                }
1135            }
1136        }"#;
1137
1138        let parsed = GenomeParser::parse(json).unwrap();
1139        assert!(parsed.classifiers.is_empty());
1140
1141        assert_eq!(parsed.cortical_areas.len(), 2);
1142
1143        // Phase 2: Verify both areas have cortical_type_new populated
1144        for area in &parsed.cortical_areas {
1145            assert!(
1146                area.cortical_id.as_cortical_type().is_ok(),
1147                "Area {} should have cortical_type_new populated",
1148                area.cortical_id
1149            );
1150        }
1151    }
1152
1153    #[test]
1154    fn test_string_to_cortical_id_legacy_power_shorthand() {
1155        // Older FEAGI genomes may encode the power core area as "___pwr" (6-char shorthand).
1156        // Migration must map this deterministically to the core Power cortical ID.
1157        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1158        let id = string_to_cortical_id("___pwr").unwrap();
1159        assert_eq!(
1160            id.as_base_64(),
1161            CoreCorticalType::Power.to_cortical_id().as_base_64()
1162        );
1163    }
1164
1165    #[test]
1166    fn test_parse_raw_imu_magnetometer_wire_id() {
1167        // Embodiment-registered Raw IMU magnetometer (subunit 2) from live FEAGI.
1168        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1169        let json = r#"{
1170            "version": "3.0",
1171            "blueprint": {
1172                "aXJpbScAAgA=": {
1173                    "cortical_name": "feagi_body_imu__Abdomen-2",
1174                    "block_boundaries": [3, 1, 10],
1175                    "relative_coordinate": [90, 0, -10],
1176                    "cortical_type": "IPU"
1177                }
1178            },
1179            "brain_regions": {}
1180        }"#;
1181
1182        let parsed = GenomeParser::parse(json).expect("Raw IMU magnetometer genome");
1183        assert_eq!(parsed.cortical_areas.len(), 1);
1184        assert_eq!(
1185            parsed.cortical_areas[0].cortical_id.as_base_64(),
1186            "aXJpbScAAgA="
1187        );
1188        parsed.cortical_areas[0]
1189            .cortical_id
1190            .as_cortical_type()
1191            .expect("magnetometer IO flag must decode");
1192    }
1193
1194    #[test]
1195    fn test_parse_positional_servo_speed_wire_id() {
1196        // Embodiment-registered Positional Servo Speed (subunit 2) from live FEAGI.
1197        // Connectome auto-save must be able to rehydrate this into a runtime genome.
1198        let json = r#"{
1199            "version": "3.0",
1200            "blueprint": {
1201                "b3BzZSEAAAA=": {
1202                    "cortical_name": "Positional Servo Speed",
1203                    "block_boundaries": [6, 1, 20],
1204                    "relative_coordinate": [-58, 0, -10],
1205                    "cortical_type": "OPU"
1206                }
1207            },
1208            "brain_regions": {}
1209        }"#;
1210
1211        let parsed = GenomeParser::parse(json).expect("Positional Servo Speed genome");
1212        assert_eq!(parsed.cortical_areas.len(), 1);
1213        assert_eq!(
1214            parsed.cortical_areas[0].cortical_id.as_base_64(),
1215            "b3BzZSEAAAA="
1216        );
1217        parsed.cortical_areas[0]
1218            .cortical_id
1219            .as_cortical_type()
1220            .expect("positional servo speed IO flag must decode");
1221    }
1222
1223    #[test]
1224    fn test_string_to_cortical_id_legacy_power_padded() {
1225        // 8-char padded form ___pwr__ (from 6-char padding in legacy flat genomes).
1226        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1227        let id = string_to_cortical_id("___pwr__").unwrap();
1228        assert_eq!(
1229            id.as_base_64(),
1230            CoreCorticalType::Power.to_cortical_id().as_base_64()
1231        );
1232    }
1233
1234    #[test]
1235    fn test_parse_with_properties() {
1236        let json = r#"{
1237            "version": "2.1",
1238            "blueprint": {
1239                "mem001": {
1240                    "cortical_name": "Memory Area",
1241                    "block_boundaries": [8, 8, 8],
1242                    "relative_coordinate": [0, 0, 0],
1243                    "cortical_type": "MEMORY",
1244                    "is_mem_type": true,
1245                    "firing_threshold": 50.0,
1246                    "leak_coefficient": 0.9
1247                }
1248            }
1249        }"#;
1250
1251        let parsed = GenomeParser::parse(json).unwrap();
1252
1253        assert_eq!(parsed.cortical_areas.len(), 1);
1254        let area = &parsed.cortical_areas[0];
1255
1256        // Old type system (deprecated)
1257        use feagi_structures::genomic::cortical_area::CorticalAreaType;
1258        assert!(matches!(area.cortical_type, CorticalAreaType::Memory(_)));
1259
1260        // Properties stored correctly
1261        assert!(area.properties.contains_key("is_mem_type"));
1262        assert!(area.properties.contains_key("firing_threshold"));
1263        assert!(area.properties.contains_key("cortical_group"));
1264
1265        // NEW: cortical_type should be derivable from cortical_id (Phase 2)
1266        assert!(
1267            area.cortical_id.as_cortical_type().is_ok(),
1268            "cortical_id should be parseable to cortical_type"
1269        );
1270        if let Ok(cortical_type) = area.cortical_id.as_cortical_type() {
1271            use feagi_structures::genomic::cortical_area::CorticalAreaType;
1272            assert!(
1273                matches!(cortical_type, CorticalAreaType::Memory(_)),
1274                "Should be classified as MEMORY type"
1275            );
1276        }
1277    }
1278
1279    /// v3 save embeds IO lists under `properties`; loading must preserve designated_inputs for BV presets.
1280    #[test]
1281    fn test_parse_v3_brain_region_nested_properties_retains_designated_io() {
1282        let json = r#"{
1283            "version": "3.0",
1284            "blueprint": {
1285                "_power": {
1286                    "cortical_name": "Core",
1287                    "block_boundaries": [10, 10, 10],
1288                    "relative_coordinate": [0, 0, 0],
1289                    "cortical_type": "CORE"
1290                }
1291            },
1292            "brain_regions": {
1293                "550e8400-e29b-41d4-a716-446655440000": {
1294                    "name": "Sub",
1295                    "cortical_areas": ["_power"],
1296                    "properties": {
1297                        "designated_inputs": ["_power"],
1298                        "designated_outputs": []
1299                    }
1300                }
1301            }
1302        }"#;
1303
1304        let parsed = GenomeParser::parse(json).unwrap();
1305        assert_eq!(parsed.brain_regions.len(), 1);
1306        let (region, _) = &parsed.brain_regions[0];
1307        let di = region
1308            .get_property("designated_inputs")
1309            .and_then(|v| v.as_array())
1310            .expect("designated_inputs");
1311        assert_eq!(di.len(), 1);
1312        assert_eq!(di[0].as_str().unwrap(), "X19fcG93ZXI=");
1313    }
1314
1315    #[test]
1316    fn test_parse_brain_region_plain_text_description() {
1317        let json = r#"{
1318            "version": "2.1",
1319            "blueprint": {
1320                "_power": {
1321                    "cortical_name": "Core",
1322                    "block_boundaries": [10, 10, 10],
1323                    "relative_coordinate": [0, 0, 0],
1324                    "cortical_type": "CORE"
1325                }
1326            },
1327            "brain_regions": {
1328                "root": {
1329                    "title": "Root",
1330                    "description": "Holds core physiology and embodiment IO",
1331                    "parent_region_id": null,
1332                    "areas": ["_power"]
1333                }
1334            }
1335        }"#;
1336
1337        let parsed = GenomeParser::parse(json).unwrap();
1338        assert_eq!(parsed.brain_regions.len(), 1);
1339        let (region, _) = &parsed.brain_regions[0];
1340        assert_eq!(
1341            region.get_property("description"),
1342            Some(&serde_json::json!(
1343                "Holds core physiology and embodiment IO"
1344            ))
1345        );
1346    }
1347
1348    #[test]
1349    fn test_invalid_version() {
1350        let json = r#"{
1351            "version": "1.0",
1352            "blueprint": {}
1353        }"#;
1354
1355        let result = GenomeParser::parse(json);
1356        assert!(result.is_err());
1357    }
1358
1359    #[test]
1360    fn test_malformed_json() {
1361        let json = r#"{ "version": "2.1", "blueprint": { malformed"#;
1362
1363        let result = GenomeParser::parse(json);
1364        assert!(result.is_err());
1365    }
1366
1367    #[test]
1368    fn test_cortical_type_new_population() {
1369        // Test that cortical_type_new field is populated during parsing (Phase 2)
1370        // This tests that parsing works with valid cortical IDs and populates types correctly
1371        use feagi_structures::genomic::cortical_area::CoreCorticalType;
1372        let power_id = CoreCorticalType::Power.to_cortical_id().as_base_64();
1373        let json = format!(
1374            r#"{{
1375            "version": "2.1",
1376            "blueprint": {{
1377                "cvision1": {{
1378                    "cortical_name": "Test Custom Vision",
1379                    "cortical_type": "CUSTOM",
1380                    "block_boundaries": [10, 10, 1],
1381                    "relative_coordinate": [0, 0, 0]
1382                }},
1383                "cmotor01": {{
1384                    "cortical_name": "Test Custom Motor",
1385                    "cortical_type": "CUSTOM",
1386                    "block_boundaries": [5, 5, 1],
1387                    "relative_coordinate": [0, 0, 0]
1388                }},
1389                "{}": {{
1390                    "cortical_name": "Test Core",
1391                    "cortical_type": "CORE",
1392                    "block_boundaries": [1, 1, 1],
1393                    "relative_coordinate": [0, 0, 0]
1394                }}
1395            }}
1396        }}"#,
1397            power_id
1398        );
1399
1400        let parsed = GenomeParser::parse(&json).unwrap();
1401        assert_eq!(parsed.cortical_areas.len(), 3);
1402
1403        // Verify all areas have cortical_type_new populated
1404        for area in &parsed.cortical_areas {
1405            assert!(
1406                area.cortical_id.as_cortical_type().is_ok(),
1407                "Area {} should have cortical_type_new populated",
1408                area.cortical_id
1409            );
1410
1411            // Verify cortical_group property is also set
1412            assert!(
1413                area.properties.contains_key("cortical_group"),
1414                "Area {} should have cortical_group property",
1415                area.cortical_id
1416            );
1417
1418            // Verify cortical group is consistent (avoid depending on feagi-brain-development)
1419            if let Some(prop_group) = area
1420                .properties
1421                .get("cortical_group")
1422                .and_then(|v| v.as_str())
1423            {
1424                assert!(
1425                    !prop_group.is_empty(),
1426                    "Area {} should have non-empty cortical_group property",
1427                    area.cortical_id.as_base_64()
1428                );
1429            }
1430        }
1431    }
1432
1433    #[test]
1434    fn test_parse_classifiers_key_parallel_to_regions() {
1435        let json = r#"{
1436            "version": "3.0",
1437            "blueprint": {
1438                "cfield": {
1439                    "cortical_name": "Field",
1440                    "cortical_type": "CUSTOM",
1441                    "block_boundaries": [4, 4, 1],
1442                    "relative_coordinate": [0, 0, 0]
1443                },
1444                "mkmem1": {
1445                    "cortical_name": "KernelMem",
1446                    "cortical_type": "MEMORY",
1447                    "block_boundaries": [2, 2, 2],
1448                    "relative_coordinate": [10, 0, 0]
1449                },
1450                "mcmem1": {
1451                    "cortical_name": "ClassMem",
1452                    "cortical_type": "MEMORY",
1453                    "block_boundaries": [2, 2, 2],
1454                    "relative_coordinate": [20, 0, 0]
1455                },
1456                "cscan1": {
1457                    "cortical_name": "ScanTwin",
1458                    "cortical_type": "CUSTOM",
1459                    "block_boundaries": [4, 4, 3],
1460                    "relative_coordinate": [30, 0, 0]
1461                }
1462            },
1463            "brain_regions": {
1464                "root": {
1465                    "title": "root",
1466                    "parent_region_id": "",
1467                    "coordinate_2d": [0, 0],
1468                    "coordinate_3d": [0, 0, 0],
1469                    "areas": ["cfield", "mkmem1", "mcmem1", "cscan1"],
1470                    "regions": [],
1471                    "inputs": [],
1472                    "outputs": []
1473                }
1474            },
1475            "classifiers": {
1476                "clf-1": {
1477                    "name": "object_class",
1478                    "parent_region_id": "root",
1479                    "coordinates_3d": [30, 0, 0],
1480                    "field_area_id": "cfield",
1481                    "kernel_memory_id": "mkmem1",
1482                    "class_memory_id": "mcmem1",
1483                    "scan_twin_id": "cscan1"
1484                }
1485            }
1486        }"#;
1487
1488        let parsed = GenomeParser::parse(json).expect("classifier genome");
1489        assert_eq!(parsed.classifiers.len(), 1);
1490        let classifier = &parsed.classifiers[0];
1491        assert_eq!(classifier.classifier_id, "clf-1");
1492        assert_eq!(classifier.name, "object_class");
1493        assert_eq!(classifier.parent_region_id, "root");
1494        assert_eq!(classifier.fields.len(), 1);
1495        assert_eq!(classifier.fields[0].field_area_id, "cfield");
1496        assert_eq!(classifier.kernel_memory_id, "mkmem1");
1497        assert_eq!(classifier.class_memory_id, "mcmem1");
1498        assert_eq!(classifier.fields[0].scan_twin_id, "cscan1");
1499        assert_eq!(classifier.owned_area_ids().len(), 3);
1500        assert_eq!(
1501            classifier.training_mode,
1502            feagi_structures::genomic::classifiers::ClassifierTrainingMode::Kernel
1503        );
1504        assert!(classifier.mask_area_id.is_none());
1505        assert!(classifier.kernel_size.is_none());
1506    }
1507
1508    #[test]
1509    fn test_parse_scanner_classifier_round_trip_fields() {
1510        let json = r#"{
1511            "version": "3.0",
1512            "blueprint": {},
1513            "brain_regions": {},
1514            "classifiers": {
1515                "clf-scan": {
1516                    "name": "scan",
1517                    "parent_region_id": "root",
1518                    "coordinates_3d": [1, 2, 3],
1519                    "training_mode": "scanner",
1520                    "mask_area_id": "cmask",
1521                    "kernel_size": [8, 8, 3],
1522                    "class_count": 19,
1523                    "kernel_memory_id": "mkmem1",
1524                    "class_memory_id": "mcmem1"
1525                }
1526            }
1527        }"#;
1528        let parsed = GenomeParser::parse(json).expect("scanner classifier");
1529        let classifier = &parsed.classifiers[0];
1530        assert_eq!(
1531            classifier.training_mode,
1532            feagi_structures::genomic::classifiers::ClassifierTrainingMode::Scanner
1533        );
1534        assert_eq!(classifier.mask_area_id.as_deref(), Some("cmask"));
1535        assert_eq!(classifier.kernel_size, Some([8, 8, 3]));
1536        assert_eq!(classifier.class_count, Some(19));
1537        assert!(classifier.kernel_area_id.is_none());
1538    }
1539
1540    #[test]
1541    fn test_parse_scanner_classifier_without_class_count_is_rejected() {
1542        let json = r#"{
1543            "version": "3.0",
1544            "blueprint": {},
1545            "brain_regions": {},
1546            "classifiers": {
1547                "clf-scan": {
1548                    "name": "scan",
1549                    "parent_region_id": "root",
1550                    "training_mode": "scanner",
1551                    "mask_area_id": "cmask",
1552                    "kernel_size": [8, 8, 3],
1553                    "kernel_memory_id": "mkmem1",
1554                    "class_memory_id": "mcmem1"
1555                }
1556            }
1557        }"#;
1558        let error = GenomeParser::parse(json).expect_err("class_count is required");
1559        assert!(error.to_string().contains("class_count"), "{error}");
1560    }
1561}