Skip to main content

feagi_services/impls/
connectome_service_impl.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Connectome service implementation.
6
7Copyright 2025 Neuraville Inc.
8Licensed under the Apache License, Version 2.0
9*/
10
11use crate::traits::ConnectomeService;
12use crate::types::*;
13use async_trait::async_trait;
14use feagi_brain_development::models::CorticalAreaExt;
15use feagi_brain_development::ConnectomeManager;
16use feagi_evolutionary::{get_default_neural_properties, MemoryAreaProperties};
17use feagi_npu_burst_engine::BurstLoopRunner;
18use feagi_structures::genomic::brain_regions::{BrainRegion, RegionID, RegionType};
19use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
20    FrameChangeHandling, PercentageNeuronPositioning,
21};
22use feagi_structures::genomic::cortical_area::CorticalID;
23use feagi_structures::genomic::cortical_area::IOCorticalAreaConfigurationFlag;
24use feagi_structures::genomic::cortical_area::{
25    CorticalArea, CorticalAreaDimensions, CorticalAreaType,
26};
27use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
28// Note: decode_cortical_id removed - use feagi_structures::CorticalID directly
29use parking_lot::RwLock;
30use serde_json::Value;
31use std::collections::{HashMap, HashSet};
32use std::sync::Arc;
33use tracing::{debug, info, trace, warn};
34
35fn derive_friendly_cortical_name(cortical_id: &CorticalID) -> Option<String> {
36    let bytes = cortical_id.as_bytes();
37    let is_input = bytes[0] == b'i';
38    let is_output = bytes[0] == b'o';
39    if !is_input && !is_output {
40        return None;
41    }
42
43    let unit_ref: [u8; 3] = [bytes[1], bytes[2], bytes[3]];
44    let subunit_index = bytes[6];
45    let unit_index = bytes[7];
46
47    if is_input {
48        for unit in SensoryCorticalUnit::list_all() {
49            if unit.get_cortical_id_unit_reference() == unit_ref {
50                let unit_name = unit.get_friendly_name();
51                let has_subunits = unit.get_number_cortical_areas() > 1;
52                let name = if has_subunits {
53                    format!(
54                        "{} Subunit {} Unit {}",
55                        unit_name, subunit_index, unit_index
56                    )
57                } else {
58                    format!("{} Unit {}", unit_name, unit_index)
59                };
60                return Some(name);
61            }
62        }
63    } else {
64        for unit in MotorCorticalUnit::list_all() {
65            if unit.get_cortical_id_unit_reference() == unit_ref {
66                let unit_name = unit.get_friendly_name();
67                let has_subunits = unit.get_number_cortical_areas() > 1;
68                let name = if matches!(unit, MotorCorticalUnit::Gaze) {
69                    let subunit_name = match subunit_index {
70                        0 => "Eccentricity",
71                        1 => "Modulation",
72                        _ => "Subunit",
73                    };
74                    format!("{} ({}) Unit {}", unit_name, subunit_name, unit_index)
75                } else if has_subunits {
76                    format!(
77                        "{} Subunit {} Unit {}",
78                        unit_name, subunit_index, unit_index
79                    )
80                } else {
81                    format!("{} Unit {}", unit_name, unit_index)
82                };
83                return Some(name);
84            }
85        }
86    }
87
88    None
89}
90
91/// Merge default template and memory properties into provided values.
92/// Existing values always override defaults.
93fn merge_memory_area_properties(
94    base: HashMap<String, Value>,
95    extra: Option<&HashMap<String, Value>>,
96) -> HashMap<String, Value> {
97    let mut defaults = get_default_neural_properties();
98    let memory_defaults = MemoryAreaProperties::default();
99    defaults
100        .entry("cortical_group".to_string())
101        .or_insert(Value::from("MEMORY"));
102    defaults
103        .entry("is_mem_type".to_string())
104        .or_insert(Value::from(true));
105    defaults
106        .entry("temporal_depth".to_string())
107        .or_insert(Value::from(memory_defaults.temporal_depth));
108    defaults
109        .entry("longterm_mem_threshold".to_string())
110        .or_insert(Value::from(memory_defaults.longterm_threshold));
111    defaults
112        .entry("lifespan_growth_rate".to_string())
113        .or_insert(Value::from(memory_defaults.lifespan_growth_rate));
114    defaults
115        .entry("init_lifespan".to_string())
116        .or_insert(Value::from(memory_defaults.init_lifespan));
117
118    defaults.extend(base);
119    if let Some(extra_props) = extra {
120        defaults.extend(extra_props.clone());
121    }
122    defaults
123}
124
125fn frame_handling_label(frame: FrameChangeHandling) -> &'static str {
126    match frame {
127        FrameChangeHandling::Absolute => "Absolute",
128        FrameChangeHandling::Incremental => "Incremental",
129    }
130}
131
132fn positioning_label(positioning: PercentageNeuronPositioning) -> &'static str {
133    match positioning {
134        PercentageNeuronPositioning::Linear => "Linear",
135        PercentageNeuronPositioning::Fractional => "Fractional",
136    }
137}
138
139fn signage_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
140    match flag {
141        IOCorticalAreaConfigurationFlag::SignedPercentage(..)
142        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(..)
143        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(..)
144        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(..) => "Percentage Signed",
145        IOCorticalAreaConfigurationFlag::Percentage(..)
146        | IOCorticalAreaConfigurationFlag::Percentage2D(..)
147        | IOCorticalAreaConfigurationFlag::Percentage3D(..)
148        | IOCorticalAreaConfigurationFlag::Percentage4D(..) => "Percentage Unsigned",
149        IOCorticalAreaConfigurationFlag::CartesianPlane(..) => "Cartesian Plane",
150        IOCorticalAreaConfigurationFlag::Misc(..) => "Misc",
151        IOCorticalAreaConfigurationFlag::Boolean => "Boolean",
152    }
153}
154
155fn behavior_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
156    match flag {
157        IOCorticalAreaConfigurationFlag::Boolean => "Not Applicable",
158        IOCorticalAreaConfigurationFlag::CartesianPlane(frame)
159        | IOCorticalAreaConfigurationFlag::Misc(frame)
160        | IOCorticalAreaConfigurationFlag::Percentage(frame, _)
161        | IOCorticalAreaConfigurationFlag::Percentage2D(frame, _)
162        | IOCorticalAreaConfigurationFlag::Percentage3D(frame, _)
163        | IOCorticalAreaConfigurationFlag::Percentage4D(frame, _)
164        | IOCorticalAreaConfigurationFlag::SignedPercentage(frame, _)
165        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(frame, _)
166        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(frame, _)
167        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(frame, _) => {
168            frame_handling_label(*frame)
169        }
170    }
171}
172
173fn coding_type_label_from_flag(flag: &IOCorticalAreaConfigurationFlag) -> &'static str {
174    match flag {
175        IOCorticalAreaConfigurationFlag::Percentage(_, positioning)
176        | IOCorticalAreaConfigurationFlag::Percentage2D(_, positioning)
177        | IOCorticalAreaConfigurationFlag::Percentage3D(_, positioning)
178        | IOCorticalAreaConfigurationFlag::Percentage4D(_, positioning)
179        | IOCorticalAreaConfigurationFlag::SignedPercentage(_, positioning)
180        | IOCorticalAreaConfigurationFlag::SignedPercentage2D(_, positioning)
181        | IOCorticalAreaConfigurationFlag::SignedPercentage3D(_, positioning)
182        | IOCorticalAreaConfigurationFlag::SignedPercentage4D(_, positioning) => {
183            positioning_label(*positioning)
184        }
185        IOCorticalAreaConfigurationFlag::CartesianPlane(_)
186        | IOCorticalAreaConfigurationFlag::Misc(_)
187        | IOCorticalAreaConfigurationFlag::Boolean => "Not Applicable",
188    }
189}
190
191fn io_unit_reference_from_cortical_id(cortical_id: &CorticalID) -> Option<[u8; 3]> {
192    let bytes = cortical_id.as_bytes();
193    if bytes[0] != b'i' && bytes[0] != b'o' {
194        return None;
195    }
196    Some([bytes[1], bytes[2], bytes[3]])
197}
198
199fn io_coding_options_for_unit(cortical_id: &CorticalID) -> Option<IOCodingOptions> {
200    let unit_ref = io_unit_reference_from_cortical_id(cortical_id)?;
201    let is_input = cortical_id.as_bytes()[0] == b'i';
202
203    let (accepted_type, allowed_frames) = if is_input {
204        let unit = SensoryCorticalUnit::list_all()
205            .iter()
206            .find(|u| u.get_cortical_id_unit_reference() == unit_ref)?;
207        (
208            unit.get_accepted_wrapped_io_data_type(),
209            unit.get_allowed_frame_change_handling(),
210        )
211    } else {
212        let unit = MotorCorticalUnit::list_all()
213            .iter()
214            .find(|u| u.get_cortical_id_unit_reference() == unit_ref)?;
215        (
216            unit.get_accepted_wrapped_io_data_type(),
217            unit.get_allowed_frame_change_handling(),
218        )
219    };
220
221    let mut signage_options = Vec::new();
222    let mut behavior_options = Vec::new();
223    let mut coding_type_options = Vec::new();
224
225    let io_flag = match cortical_id.extract_io_data_flag() {
226        Ok(flag) => flag,
227        Err(err) => {
228            warn!(
229                target: "feagi-services",
230                "[IO-CODING] {} failed to extract io_flag: {} (accepted_type={})",
231                cortical_id,
232                err,
233                accepted_type
234            );
235            return None;
236        }
237    };
238    signage_options.push(signage_label_from_flag(&io_flag).to_string());
239
240    let supports_frame_handling = !matches!(io_flag, IOCorticalAreaConfigurationFlag::Boolean);
241    if supports_frame_handling {
242        if let Some(frames) = allowed_frames {
243            for frame in frames {
244                behavior_options.push(frame_handling_label(*frame).to_string());
245            }
246        } else {
247            behavior_options.push("Absolute".to_string());
248            behavior_options.push("Incremental".to_string());
249        }
250    } else {
251        behavior_options.push("Not Applicable".to_string());
252    }
253
254    let supports_positioning = matches!(
255        io_flag,
256        IOCorticalAreaConfigurationFlag::Percentage(..)
257            | IOCorticalAreaConfigurationFlag::Percentage2D(..)
258            | IOCorticalAreaConfigurationFlag::Percentage3D(..)
259            | IOCorticalAreaConfigurationFlag::Percentage4D(..)
260            | IOCorticalAreaConfigurationFlag::SignedPercentage(..)
261            | IOCorticalAreaConfigurationFlag::SignedPercentage2D(..)
262            | IOCorticalAreaConfigurationFlag::SignedPercentage3D(..)
263            | IOCorticalAreaConfigurationFlag::SignedPercentage4D(..)
264    );
265    if supports_positioning {
266        coding_type_options.push("Linear".to_string());
267        coding_type_options.push("Fractional".to_string());
268    } else {
269        coding_type_options.push("Not Applicable".to_string());
270    }
271
272    if signage_options.is_empty() {
273        warn!(
274            target: "feagi-services",
275            "[IO-CODING] {} empty signage_options (accepted_type={}, io_flag={:?})",
276            cortical_id,
277            accepted_type,
278            io_flag
279        );
280    }
281    Some(IOCodingOptions {
282        signage_options,
283        behavior_options,
284        coding_type_options,
285    })
286}
287
288/// Update a cortical area's `cortical_mapping_dst` property in-place.
289///
290/// - When `mapping_data` is empty: remove the destination entry, and if the
291///   container becomes empty remove `cortical_mapping_dst` entirely.
292/// - When `mapping_data` is non-empty: insert/overwrite the destination entry.
293fn update_cortical_mapping_dst_in_properties(
294    properties: &mut HashMap<String, serde_json::Value>,
295    dst_area_id: &str,
296    mapping_data: &[serde_json::Value],
297) -> ServiceResult<()> {
298    if mapping_data.is_empty() {
299        let Some(existing) = properties.get_mut("cortical_mapping_dst") else {
300            return Ok(());
301        };
302        let Some(mapping_dst) = existing.as_object_mut() else {
303            return Err(ServiceError::Backend(
304                "cortical_mapping_dst is not a JSON object".to_string(),
305            ));
306        };
307
308        mapping_dst.remove(dst_area_id);
309        if mapping_dst.is_empty() {
310            properties.remove("cortical_mapping_dst");
311        }
312        return Ok(());
313    }
314
315    let entry = properties
316        .entry("cortical_mapping_dst".to_string())
317        .or_insert_with(|| serde_json::json!({}));
318
319    let Some(mapping_dst) = entry.as_object_mut() else {
320        return Err(ServiceError::Backend(
321            "cortical_mapping_dst is not a JSON object".to_string(),
322        ));
323    };
324
325    mapping_dst.insert(dst_area_id.to_string(), serde_json::json!(mapping_data));
326    Ok(())
327}
328
329fn get_cortical_mapping_dst_from_properties(
330    properties: &HashMap<String, serde_json::Value>,
331    dst_area_id: &str,
332) -> Option<Vec<serde_json::Value>> {
333    properties
334        .get("cortical_mapping_dst")
335        .and_then(|value| value.as_object())
336        .and_then(|mapping_dst| mapping_dst.get(dst_area_id))
337        .and_then(|value| value.as_array())
338        .map(|rules| rules.to_vec())
339}
340
341/// Recursively replace morphology_id in JSON values.
342///
343/// Handles object format `{"morphology_id": "x", ...}` and array format
344/// `["morphology_id", scalar, ...]` where morphology_id is the first element.
345fn replace_morphology_id_in_value(
346    value: &mut Value,
347    old_id: &str,
348    new_id: &str,
349    replaced_count: &mut usize,
350) {
351    match value {
352        Value::Object(obj) => {
353            if let Some(morphology_id) = obj.get_mut("morphology_id") {
354                if morphology_id.as_str() == Some(old_id) {
355                    *morphology_id = Value::String(new_id.to_string());
356                    *replaced_count += 1;
357                }
358            }
359            for child in obj.values_mut() {
360                replace_morphology_id_in_value(child, old_id, new_id, replaced_count);
361            }
362        }
363        Value::Array(arr) => {
364            if let Some(first) = arr.first_mut() {
365                if first.as_str() == Some(old_id) {
366                    *first = Value::String(new_id.to_string());
367                    *replaced_count += 1;
368                }
369            }
370            for child in arr.iter_mut() {
371                replace_morphology_id_in_value(child, old_id, new_id, replaced_count);
372            }
373        }
374        _ => {}
375    }
376}
377
378/// Default implementation of ConnectomeService
379pub struct ConnectomeServiceImpl {
380    connectome: Arc<RwLock<ConnectomeManager>>,
381    /// Currently loaded genome (source of truth for genome persistence)
382    /// Shared with GenomeServiceImpl to ensure cortical mappings are saved
383    current_genome: Arc<RwLock<Option<feagi_evolutionary::RuntimeGenome>>>,
384    /// Optional reference to RuntimeService for accessing NPU (for connectome I/O)
385    #[cfg(feature = "connectome-io")]
386    runtime_service: Arc<RwLock<Option<Arc<dyn crate::traits::RuntimeService + Send + Sync>>>>,
387    /// Optional burst runner for refreshing cortical_id cache
388    burst_runner: Option<Arc<RwLock<BurstLoopRunner>>>,
389}
390
391impl ConnectomeServiceImpl {
392    pub fn new(
393        connectome: Arc<RwLock<ConnectomeManager>>,
394        current_genome: Arc<RwLock<Option<feagi_evolutionary::RuntimeGenome>>>,
395    ) -> Self {
396        Self {
397            connectome,
398            current_genome,
399            #[cfg(feature = "connectome-io")]
400            runtime_service: Arc::new(RwLock::new(None)),
401            burst_runner: None,
402        }
403    }
404
405    /// Set the burst runner for cache refresh
406    pub fn set_burst_runner(&mut self, burst_runner: Arc<RwLock<BurstLoopRunner>>) {
407        self.burst_runner = Some(burst_runner);
408    }
409
410    /// Refresh cortical_id cache in burst runner
411    fn refresh_burst_runner_cache(&self) {
412        if let Some(ref burst_runner) = self.burst_runner {
413            let manager = self.connectome.read();
414            let mappings = manager.get_all_cortical_idx_to_id_mappings();
415            let chunk_sizes = manager.get_all_visualization_granularities();
416            let mapping_count = mappings.len();
417            let burst_runner_write = burst_runner.write();
418            burst_runner_write.refresh_cortical_id_mappings(mappings);
419            burst_runner_write.refresh_visualization_granularities(chunk_sizes);
420            debug!(target: "feagi-services", "Refreshed burst runner cache with {} cortical areas", mapping_count);
421        }
422    }
423
424    /// Set the runtime service (required for connectome export/import)
425    ///
426    /// This must be called after creating ConnectomeServiceImpl to enable
427    /// connectome I/O operations.
428    #[cfg(feature = "connectome-io")]
429    pub fn set_runtime_service(
430        &self,
431        runtime_service: Arc<dyn crate::traits::RuntimeService + Send + Sync>,
432    ) {
433        *self.runtime_service.write() = Some(runtime_service);
434        info!(target: "feagi-services", "RuntimeService connected to ConnectomeService for connectome I/O");
435    }
436
437    /// Convert RegionType enum to string
438    fn region_type_to_string(region_type: &RegionType) -> String {
439        match region_type {
440            RegionType::Undefined => "Undefined".to_string(),
441        }
442    }
443
444    /// Convert string to RegionType enum
445    fn string_to_region_type(s: &str) -> Result<RegionType, ServiceError> {
446        match s {
447            "Undefined" | "Sensory" | "Motor" | "Memory" | "Custom" => Ok(RegionType::Undefined),
448            _ => Err(ServiceError::InvalidInput(format!(
449                "Invalid region type: {}",
450                s
451            ))),
452        }
453    }
454}
455
456#[async_trait]
457impl ConnectomeService for ConnectomeServiceImpl {
458    // ========================================================================
459    // CORTICAL AREA OPERATIONS
460    // ========================================================================
461
462    async fn create_cortical_area(
463        &self,
464        params: CreateCorticalAreaParams,
465    ) -> ServiceResult<CorticalAreaInfo> {
466        info!(target: "feagi-services","Creating cortical area: {}", params.cortical_id);
467
468        // Convert String to CorticalID
469        let cortical_id_typed = CorticalID::try_from_base_64(&params.cortical_id)
470            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
471
472        // Get cortical area type from the cortical ID
473        let area_type = cortical_id_typed.as_cortical_type().map_err(|e| {
474            ServiceError::InvalidInput(format!("Failed to determine cortical area type: {}", e))
475        })?;
476
477        // Create CorticalArea
478        let mut area = CorticalArea::new(
479            cortical_id_typed,
480            0, // Auto-assigned by ConnectomeManager
481            params.name.clone(),
482            CorticalAreaDimensions::new(
483                params.dimensions.0 as u32,
484                params.dimensions.1 as u32,
485                params.dimensions.2 as u32,
486            )?,
487            params.position.into(), // Convert (i32, i32, i32) to GenomeCoordinate3D
488            area_type,
489        )?;
490
491        // Set the cortical type
492        // Note: cortical_type_new field removed - type is encoded in CorticalID
493
494        // Apply all neural parameters from params
495        if let Some(visible) = params.visible {
496            area.add_property_mut("visible".to_string(), serde_json::json!(visible));
497        }
498        if let Some(sub_group) = params.sub_group {
499            area.add_property_mut("sub_group".to_string(), serde_json::json!(sub_group));
500        }
501        if let Some(neurons_per_voxel) = params.neurons_per_voxel {
502            area.add_property_mut(
503                "neurons_per_voxel".to_string(),
504                serde_json::json!(neurons_per_voxel),
505            );
506        }
507        if let Some(postsynaptic_current) = params.postsynaptic_current {
508            area.add_property_mut(
509                "postsynaptic_current".to_string(),
510                serde_json::json!(postsynaptic_current),
511            );
512        }
513        if let Some(plasticity_constant) = params.plasticity_constant {
514            area.add_property_mut(
515                "plasticity_constant".to_string(),
516                serde_json::json!(plasticity_constant),
517            );
518        }
519        if let Some(degeneration) = params.degeneration {
520            area.add_property_mut("degeneration".to_string(), serde_json::json!(degeneration));
521        }
522        if let Some(psp_uniform_distribution) = params.psp_uniform_distribution {
523            area.add_property_mut(
524                "psp_uniform_distribution".to_string(),
525                serde_json::json!(psp_uniform_distribution),
526            );
527        }
528        if let Some(firing_threshold_increment) = params.firing_threshold_increment {
529            area.add_property_mut(
530                "firing_threshold_increment".to_string(),
531                serde_json::json!(firing_threshold_increment),
532            );
533        }
534        if let Some(firing_threshold_limit) = params.firing_threshold_limit {
535            area.add_property_mut(
536                "firing_threshold_limit".to_string(),
537                serde_json::json!(firing_threshold_limit),
538            );
539        }
540        if let Some(consecutive_fire_count) = params.consecutive_fire_count {
541            area.add_property_mut(
542                "consecutive_fire_limit".to_string(),
543                serde_json::json!(consecutive_fire_count),
544            );
545        }
546        if let Some(snooze_period) = params.snooze_period {
547            area.add_property_mut(
548                "snooze_period".to_string(),
549                serde_json::json!(snooze_period),
550            );
551        }
552        if let Some(refractory_period) = params.refractory_period {
553            area.add_property_mut(
554                "refractory_period".to_string(),
555                serde_json::json!(refractory_period),
556            );
557        }
558        if let Some(leak_coefficient) = params.leak_coefficient {
559            area.add_property_mut(
560                "leak_coefficient".to_string(),
561                serde_json::json!(leak_coefficient),
562            );
563        }
564        if let Some(leak_variability) = params.leak_variability {
565            area.add_property_mut(
566                "leak_variability".to_string(),
567                serde_json::json!(leak_variability),
568            );
569        }
570        if let Some(burst_engine_active) = params.burst_engine_active {
571            area.add_property_mut(
572                "burst_engine_active".to_string(),
573                serde_json::json!(burst_engine_active),
574            );
575        }
576
577        // Extract parent_region_id before moving properties
578        let parent_region_id = params
579            .properties
580            .as_ref()
581            .and_then(|props| props.get("parent_region_id"))
582            .and_then(|v| v.as_str())
583            .map(|s| s.to_string());
584
585        let is_memory_area = matches!(area_type, CorticalAreaType::Memory(_));
586        if is_memory_area {
587            let merged =
588                merge_memory_area_properties(area.properties.clone(), params.properties.as_ref());
589            area.properties = merged;
590        } else if let Some(properties) = params.properties {
591            area.properties = properties;
592        }
593
594        // Add to connectome
595        self.connectome
596            .write()
597            .add_cortical_area(area)
598            .map_err(ServiceError::from)?;
599
600        // Refresh burst runner cache after creating area
601        self.refresh_burst_runner_cache();
602
603        // CRITICAL: If parent_region_id is specified, add this cortical area
604        // to the parent brain region's cortical_areas set so it persists in genome
605        if let Some(region_id) = parent_region_id {
606            let mut manager = self.connectome.write();
607            if let Some(region) = manager.get_brain_region_mut(&region_id) {
608                region.add_area(cortical_id_typed);
609                info!(target: "feagi-services",
610                    "Added cortical area {} to parent region {}",
611                    params.cortical_id, region_id
612                );
613            } else {
614                warn!(target: "feagi-services",
615                    "Parent region {} not found for cortical area {}",
616                    region_id, params.cortical_id
617                );
618            }
619        }
620
621        // Return info
622        self.get_cortical_area(&params.cortical_id).await
623    }
624
625    async fn delete_cortical_area(&self, cortical_id: &str) -> ServiceResult<()> {
626        info!(target: "feagi-services","Deleting cortical area: {}", cortical_id);
627
628        // Convert String to CorticalID
629        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
630            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
631        let deleted_id_base64 = cortical_id_typed.as_base_64();
632        let deleted_cortical_idx = {
633            let manager = self.connectome.read();
634            manager.get_cortical_idx(&cortical_id_typed)
635        };
636        let mut removed_mapping_count = 0usize;
637        let mut removed_upstream_count = 0usize;
638
639        // Remove from the live connectome, and also scrub from brain-region membership
640        // so UI + region-based operations don't keep referencing a deleted area.
641        //
642        // Note: ConnectomeManager::remove_cortical_area currently does NOT remove the
643        // ID from brain regions, so we do it explicitly here.
644        let region_io = {
645            let mut manager = self.connectome.write();
646            let region_ids: Vec<String> = manager
647                .get_brain_region_ids()
648                .into_iter()
649                .cloned()
650                .collect();
651            for region_id in region_ids {
652                if let Some(region) = manager.get_brain_region_mut(&region_id) {
653                    region.remove_area(&cortical_id_typed);
654                }
655            }
656
657            manager
658                .remove_cortical_area(&cortical_id_typed)
659                .map_err(ServiceError::from)?;
660
661            // Scrub any remaining mappings that target the deleted area to avoid stale UI state.
662            let cortical_ids: Vec<CorticalID> = manager
663                .get_cortical_area_ids()
664                .into_iter()
665                .cloned()
666                .collect();
667            for src_id in cortical_ids {
668                let has_mapping = manager
669                    .get_cortical_area(&src_id)
670                    .and_then(|area| area.properties.get("cortical_mapping_dst"))
671                    .and_then(|value| value.as_object())
672                    .map(|mapping| mapping.contains_key(&deleted_id_base64))
673                    .unwrap_or(false);
674                if has_mapping {
675                    manager
676                        .update_cortical_mapping(&src_id, &cortical_id_typed, Vec::new())
677                        .map_err(ServiceError::from)?;
678                    removed_mapping_count += 1;
679                }
680
681                if let Some(deleted_idx) = deleted_cortical_idx {
682                    if let Some(area) = manager.get_cortical_area_mut(&src_id) {
683                        if let Some(upstream) = area
684                            .properties
685                            .get_mut("upstream_cortical_areas")
686                            .and_then(|value| value.as_array_mut())
687                        {
688                            let before = upstream.len();
689                            upstream.retain(|value| {
690                                value
691                                    .as_u64()
692                                    .map(|id| id != deleted_idx as u64)
693                                    .unwrap_or(true)
694                            });
695                            if upstream.len() != before {
696                                removed_upstream_count += before - upstream.len();
697                            }
698                        }
699                    }
700                }
701            }
702
703            Some(manager.recompute_brain_region_io_registry().map_err(|e| {
704                ServiceError::Backend(format!("Failed to recompute region IO registry: {}", e))
705            })?)
706        };
707
708        // CRITICAL: Persist deletion into RuntimeGenome (source of truth for save/export).
709        if let Some(genome) = self.current_genome.write().as_mut() {
710            let removed = genome.cortical_areas.remove(&cortical_id_typed).is_some();
711            for region in genome.brain_regions.values_mut() {
712                region.remove_area(&cortical_id_typed);
713            }
714            for area in genome.cortical_areas.values_mut() {
715                update_cortical_mapping_dst_in_properties(
716                    &mut area.properties,
717                    &deleted_id_base64,
718                    &[],
719                )?;
720                if let Some(deleted_idx) = deleted_cortical_idx {
721                    if let Some(upstream) = area
722                        .properties
723                        .get_mut("upstream_cortical_areas")
724                        .and_then(|value| value.as_array_mut())
725                    {
726                        upstream.retain(|value| {
727                            value
728                                .as_u64()
729                                .map(|id| id != deleted_idx as u64)
730                                .unwrap_or(true)
731                        });
732                    }
733                }
734            }
735            if let Some(region_io) = region_io {
736                for (region_id, (inputs, outputs)) in region_io {
737                    if let Some(region) = genome.brain_regions.get_mut(&region_id) {
738                        if inputs.is_empty() {
739                            region.properties.remove("inputs");
740                        } else {
741                            region
742                                .properties
743                                .insert("inputs".to_string(), serde_json::json!(inputs));
744                        }
745
746                        if outputs.is_empty() {
747                            region.properties.remove("outputs");
748                        } else {
749                            region
750                                .properties
751                                .insert("outputs".to_string(), serde_json::json!(outputs));
752                        }
753                    } else {
754                        warn!(
755                            target: "feagi-services",
756                            "Region '{}' not found in RuntimeGenome while persisting IO registry",
757                            region_id
758                        );
759                    }
760                }
761            }
762
763            if removed {
764                info!(
765                    target: "feagi-services",
766                    "[GENOME-UPDATE] Removed cortical area {} from RuntimeGenome",
767                    cortical_id
768                );
769            } else {
770                warn!(
771                    target: "feagi-services",
772                    "[GENOME-UPDATE] Cortical area {} not found in RuntimeGenome - deletion will not persist to saved genome",
773                    cortical_id
774                );
775            }
776        } else {
777            warn!(
778                target: "feagi-services",
779                "[GENOME-UPDATE] No RuntimeGenome loaded - deletion will not persist to saved genome"
780            );
781        }
782        if removed_mapping_count > 0 || removed_upstream_count > 0 {
783            info!(
784                target: "feagi-services",
785                "Deleted area cleanup: {} mapping references removed, {} upstream references pruned",
786                removed_mapping_count,
787                removed_upstream_count
788            );
789        }
790
791        // Refresh burst runner cache after deleting area
792        self.refresh_burst_runner_cache();
793
794        Ok(())
795    }
796
797    async fn update_cortical_area(
798        &self,
799        cortical_id: &str,
800        _params: UpdateCorticalAreaParams,
801    ) -> ServiceResult<CorticalAreaInfo> {
802        info!(target: "feagi-services","Updating cortical area: {}", cortical_id);
803
804        // TODO: This should be routed through GenomeService for proper genome update
805        // and change classification (PARAMETER vs STRUCTURAL vs METADATA)
806        // Currently this is a stub - needs architecture alignment with Python implementation
807
808        Err(ServiceError::NotImplemented(
809            "Cortical area updates must go through GenomeService for proper genome synchronization"
810                .to_string(),
811        ))
812    }
813
814    async fn get_cortical_area(&self, cortical_id: &str) -> ServiceResult<CorticalAreaInfo> {
815        trace!(target: "feagi-services", "Getting cortical area: {}", cortical_id);
816
817        // Convert String to CorticalID
818        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
819            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
820
821        let manager = self.connectome.read();
822
823        let area = manager
824            .get_cortical_area(&cortical_id_typed)
825            .ok_or_else(|| ServiceError::NotFound {
826                resource: "CorticalArea".to_string(),
827                id: cortical_id.to_string(),
828            })?;
829
830        let cortical_idx = manager
831            .get_cortical_idx(&cortical_id_typed)
832            .ok_or_else(|| ServiceError::NotFound {
833                resource: "CorticalArea".to_string(),
834                id: cortical_id.to_string(),
835            })?;
836
837        let neuron_count = manager.get_neuron_count_in_area(
838            &CorticalID::try_from_base_64(cortical_id)
839                .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?,
840        );
841        let outgoing_synapse_count = manager.get_outgoing_synapse_count_in_area(&cortical_id_typed);
842        let incoming_synapse_count = manager.get_incoming_synapse_count_in_area(&cortical_id_typed);
843        let synapse_count = outgoing_synapse_count;
844
845        // Get cortical_group from the area (uses cortical_type_new if available)
846        let cortical_group = area.get_cortical_group();
847
848        // Note: decode_cortical_id removed - IPU/OPU metadata now in CorticalID
849        let memory_props = {
850            use feagi_evolutionary::extract_memory_properties;
851            extract_memory_properties(&area.properties)
852        };
853
854        let cortical_bytes = cortical_id_typed.as_bytes();
855        let is_io_area = cortical_bytes[0] == b'i' || cortical_bytes[0] == b'o';
856        let io_flag = if is_io_area {
857            cortical_id_typed.extract_io_data_flag().ok()
858        } else {
859            None
860        };
861        let cortical_subtype = if is_io_area {
862            String::from_utf8(cortical_bytes[0..4].to_vec()).ok()
863        } else {
864            None
865        };
866        let unit_id = if is_io_area {
867            Some(cortical_bytes[6])
868        } else {
869            None
870        };
871        let coding_signage = io_flag
872            .as_ref()
873            .map(|flag| signage_label_from_flag(flag).to_string());
874        let coding_behavior = io_flag
875            .as_ref()
876            .map(|flag| behavior_label_from_flag(flag).to_string());
877        let coding_type = io_flag
878            .as_ref()
879            .map(|flag| coding_type_label_from_flag(flag).to_string());
880        let coding_options = if is_io_area {
881            io_coding_options_for_unit(&cortical_id_typed)
882        } else {
883            None
884        };
885        if is_io_area {
886            if let Some(opts) = &coding_options {
887                trace!(
888                    target: "feagi-services",
889                    "[IO-CODING] {} options signage={:?} behavior={:?} type={:?} io_flag={:?}",
890                    cortical_id,
891                    opts.signage_options,
892                    opts.behavior_options,
893                    opts.coding_type_options,
894                    io_flag
895                );
896            } else {
897                warn!(
898                    target: "feagi-services",
899                    "[IO-CODING] {} options missing (io_flag={:?})",
900                    cortical_id,
901                    io_flag
902                );
903            }
904        }
905
906        let name = if area.name.is_empty() || area.name == area.cortical_id.to_string() {
907            derive_friendly_cortical_name(&area.cortical_id).unwrap_or_else(|| area.name.clone())
908        } else {
909            area.name.clone()
910        };
911
912        let mut filtered_properties = area.properties.clone();
913        let duplicate_keys: HashSet<&str> = [
914            "coordinates_3d",
915            "cortical_dimensions",
916            "cortical_dimensions_per_device",
917            "cortical_group",
918            "group_id",
919            "sub_group_id",
920            "neurons_per_voxel",
921            "firing_threshold",
922            "firing_threshold_increment_x",
923            "firing_threshold_increment_y",
924            "firing_threshold_increment_z",
925            "firing_threshold_limit",
926            "consecutive_fire_count",
927            "refractory_period",
928            "snooze_period",
929            "leak_coefficient",
930            "leak_variability",
931            "mp_charge_accumulation",
932            "mp_driven_psp",
933            "neuron_excitability",
934            "postsynaptic_current",
935            "postsynaptic_current_max",
936            "degeneration",
937            "plasticity_constant",
938            "psp_uniform_distribution",
939            "init_lifespan",
940            "lifespan_growth_rate",
941            "longterm_mem_threshold",
942            "visible",
943        ]
944        .into_iter()
945        .collect();
946        filtered_properties.retain(|key, _| !duplicate_keys.contains(key.as_str()));
947
948        Ok(CorticalAreaInfo {
949            cortical_id: cortical_id.to_string(),
950            cortical_id_s: area.cortical_id.to_string(), // Human-readable ASCII string
951            cortical_idx,
952            name,
953            dimensions: (
954                area.dimensions.width as usize,
955                area.dimensions.height as usize,
956                area.dimensions.depth as usize,
957            ),
958            position: area.position.into(), // Convert GenomeCoordinate3D to (i32, i32, i32)
959            area_type: cortical_group
960                .clone()
961                .unwrap_or_else(|| "CUSTOM".to_string()),
962            cortical_group: cortical_group
963                .clone()
964                .unwrap_or_else(|| "CUSTOM".to_string()),
965            // Determine cortical_type based on properties
966            cortical_type: {
967                if memory_props.is_some() {
968                    "memory".to_string()
969                } else if let Some(group) = &cortical_group {
970                    match group.as_str() {
971                        "IPU" => "sensory".to_string(),
972                        "OPU" => "motor".to_string(),
973                        "CORE" => "core".to_string(),
974                        "MEMORY" => "memory".to_string(),
975                        _ => "custom".to_string(),
976                    }
977                } else {
978                    "custom".to_string()
979                }
980            },
981            neuron_count,
982            synapse_count,
983            incoming_synapse_count,
984            outgoing_synapse_count,
985            // All neural parameters come from the actual CorticalArea struct
986            visible: area.visible(),
987            sub_group: area.sub_group(),
988            neurons_per_voxel: area.neurons_per_voxel(),
989            postsynaptic_current: area.postsynaptic_current() as f64,
990            postsynaptic_current_max: area.postsynaptic_current_max() as f64,
991            plasticity_constant: area.plasticity_constant() as f64,
992            degeneration: area.degeneration() as f64,
993            psp_uniform_distribution: area.psp_uniform_distribution(),
994            mp_driven_psp: area.mp_driven_psp(),
995            firing_threshold: area.firing_threshold() as f64,
996            firing_threshold_increment: [
997                area.firing_threshold_increment_x() as f64,
998                area.firing_threshold_increment_y() as f64,
999                area.firing_threshold_increment_z() as f64,
1000            ],
1001            firing_threshold_limit: area.firing_threshold_limit() as f64,
1002            consecutive_fire_count: area.consecutive_fire_count(),
1003            snooze_period: area.snooze_period() as u32,
1004            refractory_period: area.refractory_period() as u32,
1005            leak_coefficient: area.leak_coefficient() as f64,
1006            leak_variability: area.leak_variability() as f64,
1007            mp_charge_accumulation: area.mp_charge_accumulation(),
1008            neuron_excitability: area.neuron_excitability() as f64,
1009            burst_engine_active: area.burst_engine_active(),
1010            init_lifespan: area.init_lifespan(),
1011            lifespan_growth_rate: area.lifespan_growth_rate() as f64,
1012            longterm_mem_threshold: area.longterm_mem_threshold(),
1013            temporal_depth: memory_props.map(|p| p.temporal_depth.max(1)),
1014            properties: filtered_properties,
1015            // IPU/OPU-specific decoded fields (only populated for IPU/OPU areas)
1016            cortical_subtype,
1017            encoding_type: coding_behavior.clone(),
1018            encoding_format: coding_type.clone(),
1019            unit_id,
1020            group_id: None,
1021            coding_signage,
1022            coding_behavior,
1023            coding_type,
1024            coding_options,
1025            parent_region_id: manager.get_parent_region_id_for_area(&cortical_id_typed),
1026            // Extract dev_count and cortical_dimensions_per_device from properties for IPU/OPU
1027            dev_count: area
1028                .properties
1029                .get("dev_count")
1030                .and_then(|v| v.as_u64().map(|n| n as usize)),
1031            cortical_dimensions_per_device: {
1032                // Try to get from properties first
1033                let from_properties = area
1034                    .properties
1035                    .get("cortical_dimensions_per_device")
1036                    .and_then(|v| v.as_array())
1037                    .and_then(|arr| {
1038                        if arr.len() == 3 {
1039                            Some((
1040                                arr[0].as_u64()? as usize,
1041                                arr[1].as_u64()? as usize,
1042                                arr[2].as_u64()? as usize,
1043                            ))
1044                        } else {
1045                            None
1046                        }
1047                    });
1048
1049                // If not in properties, compute from dimensions and dev_count for IPU/OPU areas
1050                if from_properties.is_none() {
1051                    if let Some(dev_count) = area
1052                        .properties
1053                        .get("dev_count")
1054                        .and_then(|v| v.as_u64().map(|n| n as usize))
1055                    {
1056                        if dev_count > 0 {
1057                            let total_width = area.dimensions.width as usize;
1058                            let height = area.dimensions.height as usize;
1059                            let depth = area.dimensions.depth as usize;
1060                            Some((total_width / dev_count, height, depth))
1061                        } else {
1062                            from_properties
1063                        }
1064                    } else {
1065                        from_properties
1066                    }
1067                } else {
1068                    from_properties
1069                }
1070            },
1071            visualization_voxel_granularity: {
1072                // Default is 1x1x1 if not in properties (user-driven, not stored)
1073                // Handle both integer and float JSON values
1074                area.properties
1075                    .get("visualization_voxel_granularity")
1076                    .and_then(|v| v.as_array())
1077                    .and_then(|arr| {
1078                        if arr.len() == 3 {
1079                            let x_opt = arr[0]
1080                                .as_u64()
1081                                .or_else(|| arr[0].as_f64().map(|f| f as u64));
1082                            let y_opt = arr[1]
1083                                .as_u64()
1084                                .or_else(|| arr[1].as_f64().map(|f| f as u64));
1085                            let z_opt = arr[2]
1086                                .as_u64()
1087                                .or_else(|| arr[2].as_f64().map(|f| f as u64));
1088                            if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1089                                Some((x as u32, y as u32, z as u32))
1090                            } else {
1091                                None
1092                            }
1093                        } else {
1094                            None
1095                        }
1096                    })
1097                    .or(Some((1, 1, 1))) // Default is 1x1x1
1098            },
1099        })
1100    }
1101
1102    async fn list_cortical_areas(&self) -> ServiceResult<Vec<CorticalAreaInfo>> {
1103        trace!(target: "feagi-services", "Listing all cortical areas");
1104
1105        let cortical_ids: Vec<String> = {
1106            let manager = self.connectome.read();
1107            manager
1108                .get_cortical_area_ids()
1109                .into_iter()
1110                .map(|id| id.as_base_64())
1111                .collect()
1112        };
1113
1114        let mut areas = Vec::new();
1115        for cortical_id in cortical_ids {
1116            if let Ok(area_info) = self.get_cortical_area(&cortical_id).await {
1117                areas.push(area_info);
1118            }
1119        }
1120
1121        Ok(areas)
1122    }
1123
1124    async fn get_cortical_area_ids(&self) -> ServiceResult<Vec<String>> {
1125        debug!(target: "feagi-services","Getting cortical area IDs");
1126
1127        // CRITICAL: Use try_read() instead of read() to avoid blocking forever
1128        // If write lock is held (e.g., during genome loading), return error instead of hanging
1129        let ids: Vec<String> = {
1130            let manager = match self.connectome.try_read() {
1131                Some(guard) => guard,
1132                None => {
1133                    warn!(target: "feagi-services", "⚠️ ConnectomeManager write lock is held - cannot read cortical area IDs");
1134                    return Err(ServiceError::Backend("ConnectomeManager is currently being modified (e.g., genome loading in progress). Please try again in a moment.".to_string()));
1135                }
1136            };
1137
1138            let area_count = manager.get_cortical_area_count();
1139            let ids_refs = manager.get_cortical_area_ids();
1140            info!(target: "feagi-services", "Found {} cortical areas in ConnectomeManager", area_count);
1141            info!(target: "feagi-services", "Cortical area IDs (references): {:?}", ids_refs.iter().take(10).collect::<Vec<_>>());
1142            ids_refs.into_iter().map(|id| id.as_base_64()).collect()
1143        }; // Lock dropped here
1144        info!(target: "feagi-services", "Returning {} cortical area IDs: {:?}", ids.len(), ids.iter().take(10).collect::<Vec<_>>());
1145        Ok(ids)
1146    }
1147
1148    async fn cortical_area_exists(&self, cortical_id: &str) -> ServiceResult<bool> {
1149        trace!(target: "feagi-services","Checking if cortical area exists: {}", cortical_id);
1150
1151        // Convert String to CorticalID
1152        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
1153            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
1154
1155        Ok(self.connectome.read().has_cortical_area(&cortical_id_typed))
1156    }
1157
1158    async fn get_cortical_area_properties(
1159        &self,
1160        cortical_id: &str,
1161    ) -> ServiceResult<std::collections::HashMap<String, serde_json::Value>> {
1162        debug!(target: "feagi-services","Getting cortical area properties: {}", cortical_id);
1163
1164        // Convert String to CorticalID
1165        let cortical_id_typed = CorticalID::try_from_base_64(cortical_id)
1166            .map_err(|e| ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e)))?;
1167
1168        let manager = self.connectome.read();
1169        manager
1170            .get_cortical_area_properties(&cortical_id_typed)
1171            .ok_or_else(|| ServiceError::NotFound {
1172                resource: "CorticalArea".to_string(),
1173                id: cortical_id.to_string(),
1174            })
1175    }
1176
1177    async fn get_all_cortical_area_properties(
1178        &self,
1179    ) -> ServiceResult<Vec<std::collections::HashMap<String, serde_json::Value>>> {
1180        debug!(target: "feagi-services","Getting all cortical area properties");
1181
1182        let manager = self.connectome.read();
1183        Ok(manager.get_all_cortical_area_properties())
1184    }
1185
1186    async fn get_neuron_properties(
1187        &self,
1188        neuron_id: u64,
1189    ) -> ServiceResult<HashMap<String, serde_json::Value>> {
1190        let manager = self.connectome.read();
1191        manager
1192            .get_neuron_properties(neuron_id)
1193            .ok_or_else(|| ServiceError::NotFound {
1194                resource: "Neuron".to_string(),
1195                id: neuron_id.to_string(),
1196            })
1197    }
1198
1199    // ========================================================================
1200    // BRAIN REGION OPERATIONS
1201    // ========================================================================
1202
1203    async fn create_brain_region(
1204        &self,
1205        params: CreateBrainRegionParams,
1206    ) -> ServiceResult<BrainRegionInfo> {
1207        info!(target: "feagi-services","Creating brain region: {}", params.region_id);
1208
1209        // Convert string to RegionType
1210        let region_type = Self::string_to_region_type(&params.region_type)?;
1211
1212        let (areas, child_regions, properties) = {
1213            let mut properties = params.properties.clone().unwrap_or_default();
1214            let area_values = properties
1215                .remove("areas")
1216                .or_else(|| properties.remove("cortical_areas"))
1217                .map(|value| {
1218                    value.as_array().cloned().ok_or_else(|| {
1219                        ServiceError::InvalidInput(
1220                            "areas must be an array of cortical area IDs".to_string(),
1221                        )
1222                    })
1223                })
1224                .transpose()?
1225                .unwrap_or_default();
1226            let child_values = properties
1227                .remove("regions")
1228                .or_else(|| properties.remove("child_regions"))
1229                .map(|value| {
1230                    value.as_array().cloned().ok_or_else(|| {
1231                        ServiceError::InvalidInput(
1232                            "regions must be an array of brain region IDs".to_string(),
1233                        )
1234                    })
1235                })
1236                .transpose()?
1237                .unwrap_or_default();
1238
1239            let mut areas: Vec<String> = Vec::new();
1240            for value in area_values {
1241                let id = value.as_str().ok_or_else(|| {
1242                    ServiceError::InvalidInput(
1243                        "areas must be an array of cortical area IDs".to_string(),
1244                    )
1245                })?;
1246                areas.push(id.to_string());
1247            }
1248
1249            let mut child_regions: Vec<String> = Vec::new();
1250            for value in child_values {
1251                let id = value.as_str().ok_or_else(|| {
1252                    ServiceError::InvalidInput(
1253                        "regions must be an array of brain region IDs".to_string(),
1254                    )
1255                })?;
1256                child_regions.push(id.to_string());
1257            }
1258
1259            (areas, child_regions, properties)
1260        };
1261
1262        // Create BrainRegion
1263        let mut region = BrainRegion::new(
1264            RegionID::from_string(&params.region_id)
1265                .map_err(|e| ServiceError::InvalidInput(format!("Invalid region ID: {}", e)))?,
1266            params.name.clone(),
1267            region_type,
1268        )
1269        .map_err(ServiceError::from)?;
1270
1271        // Apply initial properties (persisted into ConnectomeManager and RuntimeGenome).
1272        if !properties.is_empty() {
1273            region = region.with_properties(properties);
1274        }
1275
1276        // Add to connectome
1277        self.connectome
1278            .write()
1279            .add_brain_region(region, params.parent_id.clone())
1280            .map_err(ServiceError::from)?;
1281
1282        // Persist into RuntimeGenome (source of truth for genome save/export).
1283        //
1284        // NOTE: GenomeServiceImpl::create_cortical_areas requires that parent_region_id exists
1285        // in the RuntimeGenome brain_regions map. Without this, any subsequent cortical-area
1286        // creation that targets this region will fail.
1287        if let Some(genome) = self.current_genome.write().as_mut() {
1288            // Fetch the canonical region instance from ConnectomeManager to ensure any internal
1289            // normalization is reflected in the persisted copy.
1290            if let Some(created) = self
1291                .connectome
1292                .read()
1293                .get_brain_region(&params.region_id)
1294                .cloned()
1295            {
1296                genome
1297                    .brain_regions
1298                    .insert(params.region_id.clone(), created);
1299            }
1300        }
1301
1302        // Reassign areas and child regions (if provided).
1303        if !areas.is_empty() || !child_regions.is_empty() {
1304            let mut manager = self.connectome.write();
1305
1306            for area_id in &areas {
1307                let cortical_id =
1308                    feagi_evolutionary::string_to_cortical_id(area_id).map_err(|e| {
1309                        ServiceError::InvalidInput(format!("Invalid cortical ID: {}", e))
1310                    })?;
1311
1312                if !manager.has_cortical_area(&cortical_id) {
1313                    return Err(ServiceError::NotFound {
1314                        resource: "CorticalArea".to_string(),
1315                        id: area_id.clone(),
1316                    });
1317                }
1318
1319                if let Some(existing_parent) = manager.get_parent_region_id_for_area(&cortical_id) {
1320                    if let Some(parent_region) = manager.get_brain_region_mut(&existing_parent) {
1321                        parent_region.remove_area(&cortical_id);
1322                    }
1323                }
1324
1325                let Some(region) = manager.get_brain_region_mut(&params.region_id) else {
1326                    return Err(ServiceError::NotFound {
1327                        resource: "BrainRegion".to_string(),
1328                        id: params.region_id.clone(),
1329                    });
1330                };
1331                region.add_area(cortical_id);
1332
1333                if let Some(area) = manager.get_cortical_area_mut(&cortical_id) {
1334                    area.properties.insert(
1335                        "parent_region_id".to_string(),
1336                        serde_json::json!(params.region_id),
1337                    );
1338                }
1339            }
1340
1341            for child_id in &child_regions {
1342                if child_id == &params.region_id {
1343                    return Err(ServiceError::InvalidInput(
1344                        "regions cannot include the new region_id".to_string(),
1345                    ));
1346                }
1347
1348                if manager.get_brain_region(child_id).is_none() {
1349                    return Err(ServiceError::NotFound {
1350                        resource: "BrainRegion".to_string(),
1351                        id: child_id.clone(),
1352                    });
1353                }
1354
1355                manager
1356                    .change_brain_region_parent(child_id, &params.region_id)
1357                    .map_err(ServiceError::from)?;
1358
1359                if let Some(child_region) = manager.get_brain_region_mut(child_id) {
1360                    child_region.add_property(
1361                        "parent_region_id".to_string(),
1362                        serde_json::json!(params.region_id),
1363                    );
1364                }
1365            }
1366        }
1367
1368        if !areas.is_empty() || !child_regions.is_empty() {
1369            if let Some(genome) = self.current_genome.write().as_mut() {
1370                for area_id in &areas {
1371                    let Ok(cortical_id) = feagi_evolutionary::string_to_cortical_id(area_id) else {
1372                        continue;
1373                    };
1374                    for region in genome.brain_regions.values_mut() {
1375                        region.remove_area(&cortical_id);
1376                    }
1377                    if let Some(region) = genome.brain_regions.get_mut(&params.region_id) {
1378                        region.add_area(cortical_id);
1379                    }
1380                    if let Some(area) = genome.cortical_areas.get_mut(&cortical_id) {
1381                        area.properties.insert(
1382                            "parent_region_id".to_string(),
1383                            serde_json::json!(params.region_id),
1384                        );
1385                    }
1386                }
1387
1388                for child_id in &child_regions {
1389                    if let Some(child_region) = genome.brain_regions.get_mut(child_id) {
1390                        child_region.add_property(
1391                            "parent_region_id".to_string(),
1392                            serde_json::json!(params.region_id),
1393                        );
1394                    }
1395                }
1396            } else {
1397                warn!(
1398                    target: "feagi-services",
1399                    "[GENOME-UPDATE] No RuntimeGenome loaded - region membership updates will not persist to saved genome"
1400                );
1401            }
1402        }
1403
1404        // Return info
1405        self.get_brain_region(&params.region_id).await
1406    }
1407
1408    async fn delete_brain_region(&self, region_id: &str) -> ServiceResult<()> {
1409        info!(target: "feagi-services","Deleting brain region: {}", region_id);
1410
1411        let (region_ids, cortical_area_ids) = {
1412            let manager = self.connectome.read();
1413            if manager.get_brain_region(region_id).is_none() {
1414                return Err(ServiceError::NotFound {
1415                    resource: "BrainRegion".to_string(),
1416                    id: region_id.to_string(),
1417                });
1418            }
1419
1420            let hierarchy = manager.get_brain_region_hierarchy();
1421            let mut region_ids: Vec<String> = hierarchy
1422                .get_all_descendants(region_id)
1423                .into_iter()
1424                .cloned()
1425                .collect();
1426            region_ids.push(region_id.to_string());
1427
1428            let mut region_ids_with_depth: Vec<(String, usize)> = Vec::new();
1429            for region_id in &region_ids {
1430                let mut depth = 0;
1431                let mut current = region_id.as_str();
1432                while let Some(parent) = hierarchy.get_parent(current) {
1433                    depth += 1;
1434                    current = parent;
1435                }
1436                region_ids_with_depth.push((region_id.clone(), depth));
1437            }
1438            region_ids_with_depth.sort_by(|(a_id, a_depth), (b_id, b_depth)| {
1439                b_depth.cmp(a_depth).then_with(|| a_id.cmp(b_id))
1440            });
1441            let region_ids_sorted = region_ids_with_depth
1442                .into_iter()
1443                .map(|(id, _)| id)
1444                .collect::<Vec<String>>();
1445
1446            let cortical_area_ids = hierarchy
1447                .get_all_areas_recursive(region_id)
1448                .into_iter()
1449                .collect::<Vec<String>>();
1450
1451            (region_ids_sorted, cortical_area_ids)
1452        };
1453
1454        for cortical_id in cortical_area_ids {
1455            self.delete_cortical_area(&cortical_id).await?;
1456        }
1457
1458        {
1459            let mut manager = self.connectome.write();
1460            for region_id in &region_ids {
1461                manager
1462                    .remove_brain_region(region_id)
1463                    .map_err(ServiceError::from)?;
1464            }
1465        }
1466
1467        if let Some(genome) = self.current_genome.write().as_mut() {
1468            for region_id in &region_ids {
1469                genome.brain_regions.remove(region_id);
1470            }
1471        } else {
1472            warn!(
1473                target: "feagi-services",
1474                "[GENOME-UPDATE] No RuntimeGenome loaded - brain region deletions will not persist to saved genome"
1475            );
1476        }
1477
1478        Ok(())
1479    }
1480
1481    async fn update_brain_region(
1482        &self,
1483        region_id: &str,
1484        properties: std::collections::HashMap<String, serde_json::Value>,
1485    ) -> ServiceResult<BrainRegionInfo> {
1486        info!(target: "feagi-services", "Updating brain region: {}", region_id);
1487
1488        self.connectome
1489            .write()
1490            .update_brain_region_properties(region_id, properties)
1491            .map_err(ServiceError::from)?;
1492
1493        // Return updated info
1494        self.get_brain_region(region_id).await
1495    }
1496
1497    async fn get_brain_region(&self, region_id: &str) -> ServiceResult<BrainRegionInfo> {
1498        trace!(target: "feagi-services", "Getting brain region: {}", region_id);
1499
1500        let manager = self.connectome.read();
1501
1502        let region = manager
1503            .get_brain_region(region_id)
1504            .ok_or_else(|| ServiceError::NotFound {
1505                resource: "BrainRegion".to_string(),
1506                id: region_id.to_string(),
1507            })?;
1508
1509        let hierarchy = manager.get_brain_region_hierarchy();
1510        let parent_id = hierarchy.get_parent(region_id).map(|s| s.to_string());
1511        let mut child_regions: Vec<String> = hierarchy
1512            .get_children(region_id)
1513            .into_iter()
1514            .cloned()
1515            .collect();
1516        child_regions.retain(|child_id| child_id != region_id);
1517
1518        Ok(BrainRegionInfo {
1519            region_id: region_id.to_string(),
1520            name: region.name.clone(),
1521            region_type: Self::region_type_to_string(&region.region_type),
1522            parent_id,
1523            cortical_areas: region
1524                .cortical_areas
1525                .iter()
1526                .map(|id| id.as_base_64())
1527                .collect(), // Use base64 to match cortical area API
1528            child_regions,
1529            properties: region.properties.clone(),
1530        })
1531    }
1532
1533    async fn list_brain_regions(&self) -> ServiceResult<Vec<BrainRegionInfo>> {
1534        trace!(target: "feagi-services", "Listing all brain regions");
1535
1536        let region_ids: Vec<String> = {
1537            let manager = self.connectome.read();
1538            let ids = manager.get_brain_region_ids();
1539            trace!(target: "feagi-services", "Found {} brain region IDs from ConnectomeManager", ids.len());
1540            ids.into_iter().map(|s| s.to_string()).collect()
1541        };
1542
1543        trace!(target: "feagi-services", "Processing {} regions...", region_ids.len());
1544        let mut regions = Vec::new();
1545        for region_id in region_ids {
1546            trace!(target: "feagi-services", "Getting region: {}", region_id);
1547            match self.get_brain_region(&region_id).await {
1548                Ok(region_info) => {
1549                    trace!(
1550                        target: "feagi-services",
1551                        "Got region: {} with {} areas",
1552                        region_info.name,
1553                        region_info.cortical_areas.len()
1554                    );
1555                    regions.push(region_info);
1556                }
1557                Err(e) => {
1558                    warn!(target: "feagi-services", "Failed to get region {}: {}", region_id, e);
1559                }
1560            }
1561        }
1562
1563        trace!(target: "feagi-services", "Returning {} brain regions", regions.len());
1564        Ok(regions)
1565    }
1566
1567    async fn get_brain_region_ids(&self) -> ServiceResult<Vec<String>> {
1568        debug!(target: "feagi-services","Getting brain region IDs");
1569        Ok(self
1570            .connectome
1571            .read()
1572            .get_brain_region_ids()
1573            .into_iter()
1574            .map(|s| s.to_string())
1575            .collect())
1576    }
1577
1578    async fn brain_region_exists(&self, region_id: &str) -> ServiceResult<bool> {
1579        debug!(target: "feagi-services","Checking if brain region exists: {}", region_id);
1580        Ok(self.connectome.read().get_brain_region(region_id).is_some())
1581    }
1582
1583    async fn get_morphologies(&self) -> ServiceResult<HashMap<String, MorphologyInfo>> {
1584        let manager = self.connectome.read();
1585        let registry = manager.get_morphologies();
1586
1587        let mut result = HashMap::new();
1588        for (id, morphology) in registry.iter() {
1589            result.insert(
1590                id.clone(),
1591                MorphologyInfo {
1592                    morphology_type: format!("{:?}", morphology.morphology_type).to_lowercase(),
1593                    class: morphology.class.clone(),
1594                    parameters: serde_json::to_value(&morphology.parameters)
1595                        .unwrap_or(serde_json::json!({})),
1596                },
1597            );
1598        }
1599
1600        trace!(target: "feagi-services", "Retrieved {} morphologies", result.len());
1601        Ok(result)
1602    }
1603
1604    async fn create_morphology(
1605        &self,
1606        morphology_id: String,
1607        morphology: feagi_evolutionary::Morphology,
1608    ) -> ServiceResult<()> {
1609        if morphology_id.trim().is_empty() {
1610            return Err(ServiceError::InvalidInput(
1611                "morphology_id must be non-empty".to_string(),
1612            ));
1613        }
1614
1615        // Require a loaded RuntimeGenome for persistence (source of truth).
1616        let mut genome_guard = self.current_genome.write();
1617        let Some(genome) = genome_guard.as_mut() else {
1618            return Err(ServiceError::InvalidState(
1619                "No RuntimeGenome loaded - cannot create morphology".to_string(),
1620            ));
1621        };
1622
1623        if genome.morphologies.contains(&morphology_id) {
1624            return Err(ServiceError::AlreadyExists {
1625                resource: "morphology".to_string(),
1626                id: morphology_id,
1627            });
1628        }
1629
1630        genome
1631            .morphologies
1632            .add_morphology(morphology_id.clone(), morphology.clone());
1633
1634        // Keep ConnectomeManager registry in sync (used by mapping/synapse generation).
1635        self.connectome
1636            .write()
1637            .upsert_morphology(morphology_id, morphology);
1638
1639        Ok(())
1640    }
1641
1642    async fn update_morphology(
1643        &self,
1644        morphology_id: String,
1645        morphology: feagi_evolutionary::Morphology,
1646    ) -> ServiceResult<()> {
1647        if morphology_id.trim().is_empty() {
1648            return Err(ServiceError::InvalidInput(
1649                "morphology_id must be non-empty".to_string(),
1650            ));
1651        }
1652
1653        let mut genome_guard = self.current_genome.write();
1654        let Some(genome) = genome_guard.as_mut() else {
1655            return Err(ServiceError::InvalidState(
1656                "No RuntimeGenome loaded - cannot update morphology".to_string(),
1657            ));
1658        };
1659
1660        if !genome.morphologies.contains(&morphology_id) {
1661            return Err(ServiceError::NotFound {
1662                resource: "morphology".to_string(),
1663                id: morphology_id,
1664            });
1665        }
1666
1667        genome
1668            .morphologies
1669            .add_morphology(morphology_id.clone(), morphology.clone());
1670
1671        self.connectome
1672            .write()
1673            .upsert_morphology(morphology_id, morphology);
1674
1675        Ok(())
1676    }
1677
1678    async fn delete_morphology(&self, morphology_id: &str) -> ServiceResult<()> {
1679        if morphology_id.trim().is_empty() {
1680            return Err(ServiceError::InvalidInput(
1681                "morphology_id must be non-empty".to_string(),
1682            ));
1683        }
1684
1685        let mut genome_guard = self.current_genome.write();
1686        let Some(genome) = genome_guard.as_mut() else {
1687            return Err(ServiceError::InvalidState(
1688                "No RuntimeGenome loaded - cannot delete morphology".to_string(),
1689            ));
1690        };
1691
1692        if !genome.morphologies.remove_morphology(morphology_id) {
1693            return Err(ServiceError::NotFound {
1694                resource: "morphology".to_string(),
1695                id: morphology_id.to_string(),
1696            });
1697        }
1698
1699        // Mirror deletion into the ConnectomeManager registry.
1700        self.connectome.write().remove_morphology(morphology_id);
1701
1702        Ok(())
1703    }
1704
1705    async fn rename_morphology(&self, old_id: &str, new_id: &str) -> ServiceResult<()> {
1706        let old_id = old_id.trim();
1707        let new_id = new_id.trim();
1708
1709        if old_id.is_empty() {
1710            return Err(ServiceError::InvalidInput(
1711                "old_id must be non-empty".to_string(),
1712            ));
1713        }
1714        if new_id.is_empty() {
1715            return Err(ServiceError::InvalidInput(
1716                "new_id must be non-empty".to_string(),
1717            ));
1718        }
1719        if old_id == new_id {
1720            return Err(ServiceError::InvalidInput(
1721                "old_id and new_id must differ".to_string(),
1722            ));
1723        }
1724
1725        let mut genome_guard = self.current_genome.write();
1726        let Some(genome) = genome_guard.as_mut() else {
1727            return Err(ServiceError::InvalidState(
1728                "No RuntimeGenome loaded - cannot rename morphology".to_string(),
1729            ));
1730        };
1731
1732        let morphology =
1733            genome
1734                .morphologies
1735                .get(old_id)
1736                .cloned()
1737                .ok_or_else(|| ServiceError::NotFound {
1738                    resource: "morphology".to_string(),
1739                    id: old_id.to_string(),
1740                })?;
1741
1742        if morphology.class == "core" {
1743            return Err(ServiceError::InvalidInput(format!(
1744                "Core morphologies cannot be renamed: '{}'",
1745                old_id
1746            )));
1747        }
1748
1749        if genome.morphologies.contains(new_id) {
1750            return Err(ServiceError::AlreadyExists {
1751                resource: "morphology".to_string(),
1752                id: new_id.to_string(),
1753            });
1754        }
1755
1756        genome.morphologies.remove_morphology(old_id);
1757        genome
1758            .morphologies
1759            .add_morphology(new_id.to_string(), morphology.clone());
1760
1761        let mut replaced_count: usize = 0;
1762        for area in genome.cortical_areas.values_mut() {
1763            for prop_value in area.properties.values_mut() {
1764                replace_morphology_id_in_value(prop_value, old_id, new_id, &mut replaced_count);
1765            }
1766        }
1767
1768        for region in genome.brain_regions.values_mut() {
1769            for prop_value in region.properties.values_mut() {
1770                replace_morphology_id_in_value(prop_value, old_id, new_id, &mut replaced_count);
1771            }
1772        }
1773
1774        drop(genome_guard);
1775
1776        let mut manager = self.connectome.write();
1777        manager.remove_morphology(old_id);
1778        manager.upsert_morphology(new_id.to_string(), morphology);
1779
1780        info!(
1781            target: "feagi-services",
1782            "Renamed morphology '{}' to '{}' ({} references updated)",
1783            old_id,
1784            new_id,
1785            replaced_count
1786        );
1787
1788        Ok(())
1789    }
1790
1791    async fn update_cortical_mapping(
1792        &self,
1793        src_area_id: String,
1794        dst_area_id: String,
1795        mapping_data: Vec<serde_json::Value>,
1796    ) -> ServiceResult<usize> {
1797        info!(target: "feagi-services", "Updating cortical mapping: {} -> {} with {} connections",
1798              src_area_id, dst_area_id, mapping_data.len());
1799
1800        // Convert String to CorticalID
1801        use feagi_structures::genomic::cortical_area::CorticalID;
1802        let src_id = CorticalID::try_from_base_64(&src_area_id).map_err(|e| {
1803            ServiceError::InvalidInput(format!("Invalid source cortical ID: {}", e))
1804        })?;
1805        let dst_id = CorticalID::try_from_base_64(&dst_area_id).map_err(|e| {
1806            ServiceError::InvalidInput(format!("Invalid destination cortical ID: {}", e))
1807        })?;
1808
1809        let existing_mapping = {
1810            let manager = self.connectome.read();
1811            manager.get_cortical_area(&src_id).and_then(|area| {
1812                get_cortical_mapping_dst_from_properties(&area.properties, &dst_area_id)
1813            })
1814        };
1815
1816        let mapping_has_associative_memory = |rules: &[serde_json::Value]| -> bool {
1817            for rule in rules {
1818                if let Some(obj) = rule.as_object() {
1819                    if let Some(morphology_id) = obj.get("morphology_id").and_then(|v| v.as_str()) {
1820                        if morphology_id == "associative_memory" {
1821                            return true;
1822                        }
1823                    }
1824                }
1825            }
1826            false
1827        };
1828
1829        let has_associative_memory = mapping_has_associative_memory(&mapping_data)
1830            || existing_mapping
1831                .as_ref()
1832                .is_some_and(|rules| mapping_has_associative_memory(rules));
1833
1834        let mut existing_plasticity_by_morphology: HashMap<
1835            String,
1836            serde_json::Map<String, serde_json::Value>,
1837        > = HashMap::new();
1838        if let Some(existing_rules) = existing_mapping.as_ref() {
1839            for rule in existing_rules {
1840                if let Some(obj) = rule.as_object() {
1841                    if let Some(morphology_id) = obj.get("morphology_id").and_then(|v| v.as_str()) {
1842                        existing_plasticity_by_morphology
1843                            .insert(morphology_id.to_string(), obj.clone());
1844                    }
1845                }
1846            }
1847        }
1848
1849        let mut normalized_mapping_data = Vec::with_capacity(mapping_data.len());
1850        for rule in &mapping_data {
1851            if let Some(obj) = rule.as_object() {
1852                let mut normalized = obj.clone();
1853                let morphology_id = normalized
1854                    .get("morphology_id")
1855                    .and_then(|v| v.as_str())
1856                    .unwrap_or("")
1857                    .to_string();
1858                let is_associative = morphology_id == "associative_memory";
1859                let mut plasticity_flag = normalized
1860                    .get("plasticity_flag")
1861                    .and_then(|v| v.as_bool())
1862                    .unwrap_or(false);
1863                if is_associative {
1864                    normalized.insert("plasticity_flag".to_string(), serde_json::json!(true));
1865                    plasticity_flag = true;
1866                }
1867                if plasticity_flag || is_associative {
1868                    let required = [
1869                        "plasticity_constant",
1870                        "ltp_multiplier",
1871                        "ltd_multiplier",
1872                        "plasticity_window",
1873                    ];
1874                    if let Some(existing) = existing_plasticity_by_morphology.get(&morphology_id) {
1875                        for key in required {
1876                            if !normalized.contains_key(key) && existing.contains_key(key) {
1877                                if let Some(value) = existing.get(key) {
1878                                    normalized.insert(key.to_string(), value.clone());
1879                                }
1880                            }
1881                        }
1882                    }
1883                    let missing: Vec<&str> = required
1884                        .iter()
1885                        .copied()
1886                        .filter(|key| !normalized.contains_key(*key))
1887                        .collect();
1888                    if !missing.is_empty() {
1889                        return Err(ServiceError::InvalidInput(format!(
1890                            "Associative memory mapping for {} is missing required plasticity keys {:?}",
1891                            morphology_id, missing
1892                        )));
1893                    }
1894                }
1895                normalized_mapping_data.push(serde_json::Value::Object(normalized));
1896            } else {
1897                normalized_mapping_data.push(rule.clone());
1898            }
1899        }
1900
1901        debug!(
1902            target: "feagi-services",
1903            "Mapping update request: {} -> {} (existing_rules={}, new_rules={})",
1904            src_area_id,
1905            dst_area_id,
1906            existing_mapping.as_ref().map(|rules| rules.len()).unwrap_or(0),
1907            normalized_mapping_data.len()
1908        );
1909
1910        if existing_mapping
1911            .as_ref()
1912            .is_some_and(|rules| rules == &normalized_mapping_data)
1913        {
1914            info!(
1915                target: "feagi-services",
1916                "Mapping unchanged for {} -> {}; skipping regeneration",
1917                src_area_id,
1918                dst_area_id
1919            );
1920            return Ok(0);
1921        }
1922
1923        // Update RuntimeGenome if available (CRITICAL for save/load persistence!)
1924        if let Some(genome) = self.current_genome.write().as_mut() {
1925            if let Some(src_area) = genome.cortical_areas.get_mut(&src_id) {
1926                update_cortical_mapping_dst_in_properties(
1927                    &mut src_area.properties,
1928                    &dst_area_id,
1929                    &normalized_mapping_data,
1930                )?;
1931                info!(
1932                    target: "feagi-services",
1933                    "[GENOME-UPDATE] Updated cortical_mapping_dst for {} -> {} (connections={})",
1934                    src_area_id,
1935                    dst_area_id,
1936                    normalized_mapping_data.len()
1937                );
1938            } else {
1939                warn!(target: "feagi-services", "[GENOME-UPDATE] Source area {} not found in RuntimeGenome", src_area_id);
1940            }
1941        } else {
1942            warn!(target: "feagi-services", "[GENOME-UPDATE] No RuntimeGenome loaded - mapping will not persist");
1943        }
1944
1945        // Update the cortical_mapping_dst property in ConnectomeManager
1946        let region_io = {
1947            let mut manager = self.connectome.write();
1948            manager
1949                .update_cortical_mapping(&src_id, &dst_id, normalized_mapping_data.clone())
1950                .map_err(|e| ServiceError::Backend(format!("Failed to update mapping: {}", e)))?;
1951
1952            // Regenerate synapses for this mapping
1953            let synapse_count = manager
1954                .regenerate_synapses_for_mapping(&src_id, &dst_id)
1955                .map_err(|e| {
1956                    ServiceError::Backend(format!("Failed to regenerate synapses: {}", e))
1957                })?;
1958
1959            if has_associative_memory {
1960                manager
1961                    .regenerate_synapses_for_mapping(&dst_id, &src_id)
1962                    .map_err(|e| {
1963                        ServiceError::Backend(format!(
1964                            "Failed to regenerate synapses for associative mirror: {}",
1965                            e
1966                        ))
1967                    })?;
1968            }
1969
1970            // Recompute region IO registries after mapping change (critical for BV region boundary behavior)
1971            let region_io = manager.recompute_brain_region_io_registry().map_err(|e| {
1972                ServiceError::Backend(format!("Failed to recompute region IO registry: {}", e))
1973            })?;
1974
1975            info!(
1976                target: "feagi-services",
1977                "Cortical mapping updated: {} synapses created",
1978                synapse_count
1979            );
1980
1981            (synapse_count, region_io)
1982        };
1983
1984        // Refresh burst-runner caches so newly created twin areas are visualized immediately.
1985        // IMPORTANT: Call this after releasing the connectome write lock to avoid deadlocks.
1986        self.refresh_burst_runner_cache();
1987
1988        // Persist updated region IO into RuntimeGenome so genome save/export stays consistent.
1989        if let Some(genome) = self.current_genome.write().as_mut() {
1990            for (region_id, (inputs, outputs)) in region_io.1 {
1991                if let Some(region) = genome.brain_regions.get_mut(&region_id) {
1992                    if inputs.is_empty() {
1993                        region.properties.remove("inputs");
1994                    } else {
1995                        region
1996                            .properties
1997                            .insert("inputs".to_string(), serde_json::json!(inputs));
1998                    }
1999
2000                    if outputs.is_empty() {
2001                        region.properties.remove("outputs");
2002                    } else {
2003                        region
2004                            .properties
2005                            .insert("outputs".to_string(), serde_json::json!(outputs));
2006                    }
2007                } else {
2008                    warn!(
2009                        target: "feagi-services",
2010                        "Region '{}' not found in RuntimeGenome while persisting IO registry",
2011                        region_id
2012                    );
2013                }
2014            }
2015
2016            let manager = self.connectome.read();
2017            if let Some(memory_area) = manager.get_cortical_area(&dst_id) {
2018                if let Some(twin_map) = memory_area
2019                    .properties
2020                    .get("memory_twin_areas")
2021                    .and_then(|v| v.as_object())
2022                {
2023                    if let Some(genome_area) = genome.cortical_areas.get_mut(&dst_id) {
2024                        genome_area
2025                            .properties
2026                            .insert("memory_twin_areas".to_string(), serde_json::json!(twin_map));
2027                        if let Some(mapping) = memory_area.properties.get("cortical_mapping_dst") {
2028                            genome_area
2029                                .properties
2030                                .insert("cortical_mapping_dst".to_string(), mapping.clone());
2031                        }
2032                    }
2033                    for twin_id_str in twin_map.values().filter_map(|v| v.as_str()) {
2034                        let Ok(twin_id) = CorticalID::try_from_base_64(twin_id_str) else {
2035                            continue;
2036                        };
2037                        if genome.cortical_areas.contains_key(&twin_id) {
2038                            continue;
2039                        }
2040                        if let Some(twin_area) = manager.get_cortical_area(&twin_id) {
2041                            genome.cortical_areas.insert(twin_id, twin_area.clone());
2042                            if let Some(parent_region_id) = twin_area
2043                                .properties
2044                                .get("parent_region_id")
2045                                .and_then(|v| v.as_str())
2046                            {
2047                                if let Some(region) = genome.brain_regions.get_mut(parent_region_id)
2048                                {
2049                                    region.add_area(twin_id);
2050                                }
2051                            }
2052                        }
2053                    }
2054                }
2055            }
2056        }
2057
2058        Ok(region_io.0)
2059    }
2060
2061    // Note: unit tests for mapping persistence behavior are below in this module.
2062
2063    // ========================================================================
2064    // CONNECTOME I/O OPERATIONS
2065    // ========================================================================
2066
2067    #[cfg(feature = "connectome-io")]
2068    async fn export_connectome(
2069        &self,
2070    ) -> ServiceResult<feagi_npu_neural::types::connectome::ConnectomeSnapshot> {
2071        info!(target: "feagi-services", "Exporting connectome via service layer");
2072
2073        // Get NPU from ConnectomeManager (which has reference to NPU)
2074        // Note: get_npu() returns Option<&Arc<...>>, so we need to clone the Arc
2075        // to use it outside the lock scope
2076        let npu_arc = {
2077            let connectome = self.connectome.read();
2078            let npu_opt = connectome.get_npu();
2079            npu_opt
2080                .ok_or_else(|| {
2081                    ServiceError::Backend("NPU not connected to ConnectomeManager".to_string())
2082                })?
2083                .clone()
2084        };
2085
2086        // Export connectome from NPU
2087        // Note: export_connectome() is on RustNPU, but we have DynamicNPU
2088        // We need to handle both F32 and INT8 variants
2089        use tracing::debug;
2090        let lock_start = std::time::Instant::now();
2091        let thread_id = std::thread::current().id();
2092        debug!(
2093            "[NPU-LOCK] CONNECTOME-SERVICE: Thread {:?} attempting NPU lock for export_connectome at {:?}",
2094            thread_id, lock_start
2095        );
2096        let snapshot = {
2097            let npu_lock = npu_arc.lock().unwrap();
2098            let lock_acquired = std::time::Instant::now();
2099            let lock_wait = lock_acquired.duration_since(lock_start);
2100            debug!(
2101                "[NPU-LOCK] CONNECTOME-SERVICE: Thread {:?} acquired lock after {:.2}ms wait for export_connectome",
2102                thread_id,
2103                lock_wait.as_secs_f64() * 1000.0
2104            );
2105            match &*npu_lock {
2106                feagi_npu_burst_engine::DynamicNPU::F32(npu_f32) => npu_f32.export_connectome(),
2107                feagi_npu_burst_engine::DynamicNPU::INT8(npu_int8) => npu_int8.export_connectome(),
2108            }
2109        };
2110        let lock_released = std::time::Instant::now();
2111        let total_duration = lock_released.duration_since(lock_start);
2112        debug!(
2113            "[NPU-LOCK] CONNECTOME-SERVICE: Thread {:?} RELEASED NPU lock after export_connectome (total: {:.2}ms)",
2114            thread_id,
2115            total_duration.as_secs_f64() * 1000.0
2116        );
2117
2118        info!(target: "feagi-services", "✅ Connectome exported: {} neurons, {} synapses",
2119            snapshot.neurons.count, snapshot.synapses.count);
2120
2121        Ok(snapshot)
2122    }
2123
2124    #[cfg(feature = "connectome-io")]
2125    async fn import_connectome(
2126        &self,
2127        snapshot: feagi_npu_neural::types::connectome::ConnectomeSnapshot,
2128    ) -> ServiceResult<()> {
2129        info!(target: "feagi-services", "Importing connectome via service layer: {} neurons, {} synapses",
2130            snapshot.neurons.count, snapshot.synapses.count);
2131
2132        // NOTE: NPU.import_connectome_with_config() is a constructor that creates a NEW NPU.
2133        // This means importing requires replacing the entire NPU instance, which involves:
2134        // 1. Stopping the burst engine
2135        // 2. Creating a new NPU from the snapshot
2136        // 3. Replacing the NPU in ConnectomeManager and BurstLoopRunner
2137        // 4. Restarting the burst engine
2138        //
2139        // This is a complex operation that requires coordination across multiple components.
2140        // For now, we return NotImplemented and recommend using the NPU constructor directly
2141        // during application initialization, or implementing a higher-level "replace NPU" operation.
2142
2143        warn!(target: "feagi-services", "⚠️ Connectome import via service layer not yet fully implemented");
2144        warn!(target: "feagi-services", "   NPU.import_connectome_with_config() creates a new NPU instance");
2145        warn!(target: "feagi-services", "   This requires stopping burst engine, replacing NPU, and restarting");
2146        warn!(target: "feagi-services", "   Recommendation: Use NPU.import_connectome_with_config() during initialization");
2147
2148        Err(ServiceError::NotImplemented(
2149            "Connectome import via service layer requires NPU replacement coordination. Use NPU.import_connectome_with_config() during application initialization, or implement a 'replace NPU' operation that coordinates with BurstLoopRunner.".to_string(),
2150        ))
2151    }
2152}
2153
2154#[cfg(test)]
2155mod tests {
2156    use super::{replace_morphology_id_in_value, update_cortical_mapping_dst_in_properties};
2157    use crate::types::ServiceResult;
2158    use std::collections::HashMap;
2159
2160    #[test]
2161    fn empty_mapping_deletes_destination_key_and_prunes_container() -> ServiceResult<()> {
2162        let mut props: HashMap<String, serde_json::Value> = HashMap::new();
2163        props.insert(
2164            "cortical_mapping_dst".to_string(),
2165            serde_json::json!({
2166                "dstA": [{"morphology_id": "m1"}],
2167                "dstB": []
2168            }),
2169        );
2170
2171        update_cortical_mapping_dst_in_properties(&mut props, "dstA", &[])?;
2172        let dst = props
2173            .get("cortical_mapping_dst")
2174            .and_then(|v| v.as_object())
2175            .expect("cortical_mapping_dst should remain with dstB");
2176        assert!(!dst.contains_key("dstA"));
2177        assert!(dst.contains_key("dstB"));
2178
2179        // Now remove last remaining destination, container should be removed entirely
2180        update_cortical_mapping_dst_in_properties(&mut props, "dstB", &[])?;
2181        assert!(!props.contains_key("cortical_mapping_dst"));
2182        Ok(())
2183    }
2184
2185    #[test]
2186    fn non_empty_mapping_sets_destination_key() -> ServiceResult<()> {
2187        let mut props: HashMap<String, serde_json::Value> = HashMap::new();
2188        update_cortical_mapping_dst_in_properties(
2189            &mut props,
2190            "dstX",
2191            &[serde_json::json!({"morphology_id": "m1"})],
2192        )?;
2193
2194        let dst = props
2195            .get("cortical_mapping_dst")
2196            .and_then(|v| v.as_object())
2197            .expect("cortical_mapping_dst should exist");
2198        let arr = dst
2199            .get("dstX")
2200            .and_then(|v| v.as_array())
2201            .expect("dstX should be an array");
2202        assert_eq!(arr.len(), 1);
2203        Ok(())
2204    }
2205
2206    #[tokio::test]
2207    async fn morphology_create_update_delete_roundtrip() -> ServiceResult<()> {
2208        use super::ConnectomeServiceImpl;
2209        use crate::traits::ConnectomeService;
2210        use parking_lot::RwLock;
2211        use std::sync::Arc;
2212
2213        // Isolated connectome manager instance for this test.
2214        let connectome = Arc::new(RwLock::new(
2215            feagi_brain_development::ConnectomeManager::new_for_testing(),
2216        ));
2217
2218        // Minimal RuntimeGenome (source of truth) for persistence.
2219        let genome = feagi_evolutionary::RuntimeGenome {
2220            metadata: feagi_evolutionary::GenomeMetadata {
2221                genome_id: "test".to_string(),
2222                genome_title: "test".to_string(),
2223                genome_description: "".to_string(),
2224                version: "2.0".to_string(),
2225                timestamp: 0.0,
2226                brain_regions_root: None,
2227            },
2228            cortical_areas: HashMap::new(),
2229            brain_regions: HashMap::new(),
2230            morphologies: feagi_evolutionary::MorphologyRegistry::new(),
2231            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2232            signatures: feagi_evolutionary::GenomeSignatures {
2233                genome: "0".to_string(),
2234                blueprint: "0".to_string(),
2235                physiology: "0".to_string(),
2236                morphologies: None,
2237            },
2238            stats: feagi_evolutionary::GenomeStats::default(),
2239        };
2240        let current_genome = Arc::new(RwLock::new(Some(genome)));
2241
2242        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2243
2244        // Create
2245        let morph_id = "m_test_vectors".to_string();
2246        let morph = feagi_evolutionary::Morphology {
2247            morphology_type: feagi_evolutionary::MorphologyType::Vectors,
2248            parameters: feagi_evolutionary::MorphologyParameters::Vectors {
2249                vectors: vec![[1, 2, 3]],
2250            },
2251            class: "custom".to_string(),
2252        };
2253        svc.create_morphology(morph_id.clone(), morph).await?;
2254
2255        // Verify both source-of-truth and connectome registry were updated
2256        {
2257            let genome_guard = current_genome.read();
2258            let genome = genome_guard.as_ref().expect("genome must exist");
2259            assert!(genome.morphologies.contains(&morph_id));
2260        }
2261        {
2262            let mgr = connectome.read();
2263            assert!(mgr.get_morphologies().contains(&morph_id));
2264        }
2265
2266        // Update (overwrite vectors)
2267        let morph2 = feagi_evolutionary::Morphology {
2268            morphology_type: feagi_evolutionary::MorphologyType::Vectors,
2269            parameters: feagi_evolutionary::MorphologyParameters::Vectors {
2270                vectors: vec![[9, 9, 9]],
2271            },
2272            class: "custom".to_string(),
2273        };
2274        svc.update_morphology(morph_id.clone(), morph2).await?;
2275        {
2276            let mgr = connectome.read();
2277            let stored = mgr
2278                .get_morphologies()
2279                .get(&morph_id)
2280                .expect("morphology must exist");
2281            match &stored.parameters {
2282                feagi_evolutionary::MorphologyParameters::Vectors { vectors } => {
2283                    assert_eq!(vectors.as_slice(), &[[9, 9, 9]]);
2284                }
2285                other => panic!("unexpected parameters: {:?}", other),
2286            }
2287        }
2288
2289        // Delete
2290        svc.delete_morphology(&morph_id).await?;
2291        {
2292            let genome_guard = current_genome.read();
2293            let genome = genome_guard.as_ref().expect("genome must exist");
2294            assert!(!genome.morphologies.contains(&morph_id));
2295        }
2296        {
2297            let mgr = connectome.read();
2298            assert!(!mgr.get_morphologies().contains(&morph_id));
2299        }
2300
2301        Ok(())
2302    }
2303
2304    #[tokio::test]
2305    async fn morphology_rename_updates_registry_and_cortical_mapping_references(
2306    ) -> ServiceResult<()> {
2307        use super::ConnectomeServiceImpl;
2308        use crate::traits::ConnectomeService;
2309        use feagi_structures::genomic::cortical_area::{
2310            CorticalArea, CorticalAreaDimensions, CorticalAreaType, CorticalID,
2311        };
2312        use parking_lot::RwLock;
2313        use std::collections::HashMap;
2314        use std::sync::Arc;
2315
2316        let connectome = Arc::new(RwLock::new(
2317            feagi_brain_development::ConnectomeManager::new_for_testing(),
2318        ));
2319
2320        let src_id = CorticalID::try_from_bytes(b"csrc0001").unwrap();
2321        let dst_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
2322
2323        let mut src_area = CorticalArea::new(
2324            src_id,
2325            0,
2326            "Source".to_string(),
2327            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
2328            (0, 0, 0).into(),
2329            CorticalAreaType::Custom(
2330                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
2331            ),
2332        )
2333        .unwrap();
2334        src_area.properties.insert(
2335            "cortical_mapping_dst".to_string(),
2336            serde_json::json!({
2337                dst_id.as_base_64(): [
2338                    {"morphology_id": "m_old", "morphology_scalar": 1},
2339                    ["m_old", 2, 1.0, false]
2340                ]
2341            }),
2342        );
2343
2344        let dst_area = CorticalArea::new(
2345            dst_id,
2346            1,
2347            "Target".to_string(),
2348            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
2349            (0, 0, 0).into(),
2350            CorticalAreaType::Custom(
2351                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
2352            ),
2353        )
2354        .unwrap();
2355
2356        let morph = feagi_evolutionary::Morphology {
2357            morphology_type: feagi_evolutionary::MorphologyType::Vectors,
2358            parameters: feagi_evolutionary::MorphologyParameters::Vectors {
2359                vectors: vec![[1, 0, 0]],
2360            },
2361            class: "custom".to_string(),
2362        };
2363
2364        let mut morphologies = feagi_evolutionary::MorphologyRegistry::new();
2365        morphologies.add_morphology("m_old".to_string(), morph.clone());
2366
2367        let genome = feagi_evolutionary::RuntimeGenome {
2368            metadata: feagi_evolutionary::GenomeMetadata {
2369                genome_id: "test".to_string(),
2370                genome_title: "test".to_string(),
2371                genome_description: "".to_string(),
2372                version: "2.0".to_string(),
2373                timestamp: 0.0,
2374                brain_regions_root: None,
2375            },
2376            cortical_areas: HashMap::from([(src_id, src_area.clone()), (dst_id, dst_area.clone())]),
2377            brain_regions: HashMap::new(),
2378            morphologies,
2379            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2380            signatures: feagi_evolutionary::GenomeSignatures {
2381                genome: "0".to_string(),
2382                blueprint: "0".to_string(),
2383                physiology: "0".to_string(),
2384                morphologies: None,
2385            },
2386            stats: feagi_evolutionary::GenomeStats::default(),
2387        };
2388
2389        {
2390            let mut manager = connectome.write();
2391            manager.upsert_morphology("m_old".to_string(), morph);
2392        }
2393
2394        let current_genome = Arc::new(RwLock::new(Some(genome)));
2395        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2396
2397        svc.rename_morphology("m_old", "m_new").await?;
2398
2399        {
2400            let genome_guard = current_genome.read();
2401            let genome = genome_guard.as_ref().expect("genome must exist");
2402            assert!(!genome.morphologies.contains("m_old"));
2403            assert!(genome.morphologies.contains("m_new"));
2404
2405            let area = genome
2406                .cortical_areas
2407                .get(&src_id)
2408                .expect("src area must exist");
2409            let dstmap = area
2410                .properties
2411                .get("cortical_mapping_dst")
2412                .and_then(|v| v.as_object())
2413                .expect("cortical_mapping_dst must exist");
2414            let rules = dstmap
2415                .get(&dst_id.as_base_64())
2416                .and_then(|v| v.as_array())
2417                .expect("rules must exist");
2418            assert_eq!(rules.len(), 2);
2419            assert_eq!(
2420                rules[0].get("morphology_id").and_then(|v| v.as_str()),
2421                Some("m_new")
2422            );
2423            assert_eq!(
2424                rules[1]
2425                    .as_array()
2426                    .and_then(|a| a.first())
2427                    .and_then(|v| v.as_str()),
2428                Some("m_new")
2429            );
2430        }
2431
2432        {
2433            let mgr = connectome.read();
2434            assert!(!mgr.get_morphologies().contains("m_old"));
2435            assert!(mgr.get_morphologies().contains("m_new"));
2436        }
2437
2438        Ok(())
2439    }
2440
2441    #[tokio::test]
2442    async fn morphology_rename_rejects_core_morphologies() -> ServiceResult<()> {
2443        use super::ConnectomeServiceImpl;
2444        use crate::traits::ConnectomeService;
2445        use crate::types::ServiceError;
2446        use parking_lot::RwLock;
2447        use std::collections::HashMap;
2448        use std::sync::Arc;
2449
2450        let connectome = Arc::new(RwLock::new(
2451            feagi_brain_development::ConnectomeManager::new_for_testing(),
2452        ));
2453        {
2454            let mut manager = connectome.write();
2455            manager.setup_core_morphologies_for_testing();
2456        }
2457
2458        let mut morphologies = feagi_evolutionary::MorphologyRegistry::new();
2459        feagi_evolutionary::add_core_morphologies(&mut morphologies);
2460
2461        let genome = feagi_evolutionary::RuntimeGenome {
2462            metadata: feagi_evolutionary::GenomeMetadata {
2463                genome_id: "test".to_string(),
2464                genome_title: "test".to_string(),
2465                genome_description: "".to_string(),
2466                version: "2.0".to_string(),
2467                timestamp: 0.0,
2468                brain_regions_root: None,
2469            },
2470            cortical_areas: HashMap::new(),
2471            brain_regions: HashMap::new(),
2472            morphologies,
2473            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2474            signatures: feagi_evolutionary::GenomeSignatures {
2475                genome: "0".to_string(),
2476                blueprint: "0".to_string(),
2477                physiology: "0".to_string(),
2478                morphologies: None,
2479            },
2480            stats: feagi_evolutionary::GenomeStats::default(),
2481        };
2482
2483        let current_genome = Arc::new(RwLock::new(Some(genome)));
2484        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2485
2486        let err = svc
2487            .rename_morphology("projector", "projector_renamed")
2488            .await
2489            .unwrap_err();
2490        assert!(
2491            matches!(err, ServiceError::InvalidInput(_)),
2492            "Expected InvalidInput, got {:?}",
2493            err
2494        );
2495
2496        Ok(())
2497    }
2498
2499    #[test]
2500    fn replace_morphology_id_in_value_handles_object_and_array_formats() {
2501        let mut value = serde_json::json!({
2502            "nested": {
2503                "morphology_id": "old_id",
2504                "other": "x"
2505            },
2506            "arr": ["old_id", 1, 2]
2507        });
2508        let mut count = 0;
2509        replace_morphology_id_in_value(&mut value, "old_id", "new_id", &mut count);
2510        assert_eq!(count, 2);
2511        assert_eq!(value["nested"]["morphology_id"], "new_id");
2512        assert_eq!(value["arr"][0], "new_id");
2513    }
2514
2515    #[tokio::test]
2516    async fn associative_memory_missing_window_is_filled_from_existing_mapping() -> ServiceResult<()>
2517    {
2518        use super::ConnectomeServiceImpl;
2519        use crate::traits::ConnectomeService;
2520        use feagi_structures::genomic::cortical_area::{
2521            CorticalArea, CorticalAreaDimensions, CorticalAreaType, CorticalID,
2522        };
2523        use parking_lot::RwLock;
2524        use std::collections::HashMap;
2525        use std::sync::Arc;
2526
2527        let connectome = Arc::new(RwLock::new(
2528            feagi_brain_development::ConnectomeManager::new_for_testing(),
2529        ));
2530
2531        let src_id = CorticalID::try_from_bytes(b"csrc0003").unwrap();
2532        let dst_id = CorticalID::try_from_bytes(b"csrc0004").unwrap();
2533
2534        let mut src_area = CorticalArea::new(
2535            src_id,
2536            0,
2537            "Source".to_string(),
2538            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
2539            (0, 0, 0).into(),
2540            CorticalAreaType::Custom(
2541                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
2542            ),
2543        )
2544        .unwrap();
2545        let dst_area = CorticalArea::new(
2546            dst_id,
2547            0,
2548            "Target".to_string(),
2549            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
2550            (0, 0, 0).into(),
2551            CorticalAreaType::Custom(
2552                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
2553            ),
2554        )
2555        .unwrap();
2556
2557        src_area.properties.insert(
2558            "cortical_mapping_dst".to_string(),
2559            serde_json::json!({
2560                dst_id.as_base_64(): [{
2561                    "morphology_id": "associative_memory",
2562                    "morphology_scalar": 1,
2563                    "postSynapticCurrent_multiplier": 1.0,
2564                    "plasticity_flag": true,
2565                    "plasticity_constant": 1,
2566                    "ltp_multiplier": 1,
2567                    "ltd_multiplier": 1,
2568                    "plasticity_window": 5
2569                }]
2570            }),
2571        );
2572
2573        {
2574            let mut manager = connectome.write();
2575            manager.add_cortical_area(src_area.clone())?;
2576            manager.add_cortical_area(dst_area.clone())?;
2577        }
2578
2579        let genome = feagi_evolutionary::RuntimeGenome {
2580            metadata: feagi_evolutionary::GenomeMetadata {
2581                genome_id: "test".to_string(),
2582                genome_title: "test".to_string(),
2583                genome_description: "".to_string(),
2584                version: "2.0".to_string(),
2585                timestamp: 0.0,
2586                brain_regions_root: None,
2587            },
2588            cortical_areas: HashMap::from([(src_id, src_area), (dst_id, dst_area)]),
2589            brain_regions: HashMap::new(),
2590            morphologies: feagi_evolutionary::MorphologyRegistry::new(),
2591            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2592            signatures: feagi_evolutionary::GenomeSignatures {
2593                genome: "0".to_string(),
2594                blueprint: "0".to_string(),
2595                physiology: "0".to_string(),
2596                morphologies: None,
2597            },
2598            stats: feagi_evolutionary::GenomeStats::default(),
2599        };
2600        let current_genome = Arc::new(RwLock::new(Some(genome)));
2601
2602        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2603
2604        let mapping_data = vec![serde_json::json!({
2605            "morphology_id": "associative_memory",
2606            "morphology_scalar": 1,
2607            "postSynapticCurrent_multiplier": 1.0,
2608            "plasticity_flag": true,
2609            "plasticity_constant": 1,
2610            "ltp_multiplier": 1,
2611            "ltd_multiplier": 1
2612        })];
2613
2614        svc.update_cortical_mapping(src_id.as_base_64(), dst_id.as_base_64(), mapping_data)
2615            .await?;
2616
2617        let genome_guard = current_genome.read();
2618        let genome = genome_guard.as_ref().expect("genome must exist");
2619        let updated = genome
2620            .cortical_areas
2621            .get(&src_id)
2622            .and_then(|area| area.properties.get("cortical_mapping_dst"))
2623            .and_then(|v| v.as_object())
2624            .and_then(|map| map.get(&dst_id.as_base_64()))
2625            .and_then(|v| v.as_array())
2626            .and_then(|rules| rules.first())
2627            .and_then(|v| v.as_object())
2628            .and_then(|obj| obj.get("plasticity_window"))
2629            .and_then(|v| v.as_i64());
2630        assert_eq!(updated, Some(5));
2631
2632        Ok(())
2633    }
2634
2635    #[tokio::test]
2636    async fn delete_cortical_area_persists_to_runtime_genome() -> ServiceResult<()> {
2637        use super::ConnectomeServiceImpl;
2638        use crate::traits::ConnectomeService;
2639        use feagi_structures::genomic::brain_regions::{BrainRegion, RegionID, RegionType};
2640        use feagi_structures::genomic::cortical_area::{
2641            CoreCorticalType, CorticalArea, CorticalAreaDimensions,
2642        };
2643        use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
2644        use parking_lot::RwLock;
2645        use std::collections::HashMap;
2646        use std::sync::Arc;
2647
2648        // Isolated connectome manager instance for this test.
2649        let connectome = Arc::new(RwLock::new(
2650            feagi_brain_development::ConnectomeManager::new_for_testing(),
2651        ));
2652
2653        // Use a known-valid cortical ID/type pair to avoid ID encoding intricacies in this unit test.
2654        let cortical_id = CoreCorticalType::Power.to_cortical_id();
2655
2656        let dims = CorticalAreaDimensions::new(1, 1, 1).expect("dimensions must be valid");
2657        let pos = GenomeCoordinate3D::new(0, 0, 0);
2658        let cortical_type = cortical_id
2659            .as_cortical_type()
2660            .expect("cortical type must be derivable from id");
2661
2662        let area = CorticalArea::new(
2663            cortical_id,
2664            0, // Let ConnectomeManager assign a proper idx
2665            "test_area".to_string(),
2666            dims,
2667            pos,
2668            cortical_type,
2669        )
2670        .expect("area must be valid");
2671
2672        // Create a region that contains the test area.
2673        let region_id = RegionID::new();
2674        let region_key = region_id.to_string();
2675        let region = BrainRegion::new(region_id, "root".to_string(), RegionType::Undefined)
2676            .expect("region must be valid")
2677            .with_areas([cortical_id]);
2678
2679        // Seed RuntimeGenome with the area + region membership (this is what genome save/export uses).
2680        let genome = feagi_evolutionary::RuntimeGenome {
2681            metadata: feagi_evolutionary::GenomeMetadata {
2682                genome_id: "test".to_string(),
2683                genome_title: "test".to_string(),
2684                genome_description: "".to_string(),
2685                version: "3.0".to_string(),
2686                timestamp: 0.0,
2687                brain_regions_root: Some(region_key.clone()),
2688            },
2689            cortical_areas: HashMap::from([(cortical_id, area.clone())]),
2690            brain_regions: HashMap::from([(region_key.clone(), region.clone())]),
2691            morphologies: feagi_evolutionary::MorphologyRegistry::new(),
2692            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2693            signatures: feagi_evolutionary::GenomeSignatures {
2694                genome: "0".to_string(),
2695                blueprint: "0".to_string(),
2696                physiology: "0".to_string(),
2697                morphologies: None,
2698            },
2699            stats: feagi_evolutionary::GenomeStats::default(),
2700        };
2701        let current_genome = Arc::new(RwLock::new(Some(genome)));
2702
2703        // Seed ConnectomeManager with the same region + area (this is what BV and runtime uses).
2704        {
2705            let mut mgr = connectome.write();
2706            mgr.add_brain_region(region, None)
2707                .expect("brain region should be addable");
2708            mgr.add_cortical_area(area)
2709                .expect("cortical area should be addable");
2710        }
2711
2712        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2713
2714        // Act: delete by base64 string.
2715        let cortical_id_base64 = cortical_id.as_base_64();
2716        svc.delete_cortical_area(&cortical_id_base64).await?;
2717
2718        // Assert: RuntimeGenome no longer contains the area nor region membership.
2719        {
2720            let genome_guard = current_genome.read();
2721            let genome = genome_guard.as_ref().expect("genome must exist");
2722            assert!(!genome.cortical_areas.contains_key(&cortical_id));
2723            let region = genome
2724                .brain_regions
2725                .get(&region_key)
2726                .expect("region must exist in genome");
2727            assert!(!region.contains_area(&cortical_id));
2728        }
2729
2730        Ok(())
2731    }
2732
2733    /// Deleting a region should remove descendants, areas, and RuntimeGenome entries.
2734    #[tokio::test]
2735    async fn delete_brain_region_deletes_descendants_and_persists() -> ServiceResult<()> {
2736        use super::ConnectomeServiceImpl;
2737        use crate::traits::ConnectomeService;
2738        use feagi_brain_development::ConnectomeManager;
2739        use feagi_structures::genomic::brain_regions::{BrainRegion, RegionID, RegionType};
2740        use feagi_structures::genomic::cortical_area::{
2741            CoreCorticalType, CorticalArea, CorticalAreaDimensions,
2742        };
2743        use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
2744        use parking_lot::RwLock;
2745        use std::collections::HashMap;
2746        use std::sync::Arc;
2747
2748        let connectome = Arc::new(RwLock::new(ConnectomeManager::new_for_testing()));
2749
2750        let root_id = RegionID::new();
2751        let root_key = root_id.to_string();
2752        let child_id = RegionID::new();
2753        let child_key = child_id.to_string();
2754        let grandchild_id = RegionID::new();
2755        let grandchild_key = grandchild_id.to_string();
2756
2757        let power_id = CoreCorticalType::Power.to_cortical_id();
2758        let death_id = CoreCorticalType::Death.to_cortical_id();
2759
2760        let power_area = CorticalArea::new(
2761            power_id,
2762            0,
2763            "power_area".to_string(),
2764            CorticalAreaDimensions::new(1, 1, 1)?,
2765            GenomeCoordinate3D::new(0, 0, 0),
2766            power_id.as_cortical_type()?,
2767        )?;
2768        let death_area = CorticalArea::new(
2769            death_id,
2770            0,
2771            "death_area".to_string(),
2772            CorticalAreaDimensions::new(1, 1, 1)?,
2773            GenomeCoordinate3D::new(0, 0, 0),
2774            death_id.as_cortical_type()?,
2775        )?;
2776
2777        let root = BrainRegion::new(root_id, "root".to_string(), RegionType::Undefined)?;
2778        let child = BrainRegion::new(child_id, "child".to_string(), RegionType::Undefined)?
2779            .with_areas([power_id]);
2780        let grandchild =
2781            BrainRegion::new(grandchild_id, "grand".to_string(), RegionType::Undefined)?
2782                .with_areas([death_id]);
2783
2784        let genome = feagi_evolutionary::RuntimeGenome {
2785            metadata: feagi_evolutionary::GenomeMetadata {
2786                genome_id: "test".to_string(),
2787                genome_title: "test".to_string(),
2788                genome_description: "".to_string(),
2789                version: "3.0".to_string(),
2790                timestamp: 0.0,
2791                brain_regions_root: Some(root_key.clone()),
2792            },
2793            cortical_areas: HashMap::from([
2794                (power_id, power_area.clone()),
2795                (death_id, death_area.clone()),
2796            ]),
2797            brain_regions: HashMap::from([
2798                (root_key.clone(), root.clone()),
2799                (child_key.clone(), child.clone()),
2800                (grandchild_key.clone(), grandchild.clone()),
2801            ]),
2802            morphologies: feagi_evolutionary::MorphologyRegistry::new(),
2803            physiology: feagi_evolutionary::PhysiologyConfig::default(),
2804            signatures: feagi_evolutionary::GenomeSignatures {
2805                genome: "0".to_string(),
2806                blueprint: "0".to_string(),
2807                physiology: "0".to_string(),
2808                morphologies: None,
2809            },
2810            stats: feagi_evolutionary::GenomeStats::default(),
2811        };
2812        let current_genome = Arc::new(RwLock::new(Some(genome)));
2813
2814        {
2815            let mut manager = connectome.write();
2816            manager.add_brain_region(root, None)?;
2817            manager.add_brain_region(child, Some(root_key.clone()))?;
2818            manager.add_brain_region(grandchild, Some(child_key.clone()))?;
2819            manager.add_cortical_area(power_area)?;
2820            manager.add_cortical_area(death_area)?;
2821        }
2822
2823        let svc = ConnectomeServiceImpl::new(connectome.clone(), current_genome.clone());
2824        svc.delete_brain_region(&child_key).await?;
2825
2826        {
2827            let manager = connectome.read();
2828            assert!(manager.get_brain_region(&child_key).is_none());
2829            assert!(manager.get_brain_region(&grandchild_key).is_none());
2830            assert!(manager.get_brain_region(&root_key).is_some());
2831            assert!(!manager.has_cortical_area(&power_id));
2832            assert!(!manager.has_cortical_area(&death_id));
2833        }
2834
2835        {
2836            let genome_guard = current_genome.read();
2837            let genome = genome_guard.as_ref().expect("genome must exist");
2838            assert!(!genome.brain_regions.contains_key(&child_key));
2839            assert!(!genome.brain_regions.contains_key(&grandchild_key));
2840            assert!(genome.brain_regions.contains_key(&root_key));
2841            assert!(!genome.cortical_areas.contains_key(&power_id));
2842            assert!(!genome.cortical_areas.contains_key(&death_id));
2843        }
2844
2845        Ok(())
2846    }
2847}