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