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