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