Skip to main content

feagi_brain_development/
connectome_manager.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5ConnectomeManager - Core brain connectivity manager.
6
7This is the central orchestrator for the FEAGI connectome, managing:
8- Cortical areas and their metadata
9- Brain regions and hierarchy
10- Neuron/synapse queries (delegates to NPU for actual data)
11- Genome loading and persistence
12
13## Architecture
14
15The ConnectomeManager is a **metadata manager** that:
161. Stores cortical area/region definitions
172. Provides a high-level API for brain structure queries
183. Delegates neuron/synapse CRUD to the NPU (Structure of Arrays)
19
20## Design Principles
21
22- **Singleton**: One global instance per FEAGI process
23- **Thread-safe**: Uses RwLock for concurrent reads
24- **Performance**: Optimized for hot-path queries (area lookups)
25- **NPU Delegation**: Neuron/synapse data lives in NPU, not here
26
27Copyright 2025 Neuraville Inc.
28Licensed under the Apache License, Version 2.0
29*/
30
31use once_cell::sync::Lazy;
32use parking_lot::RwLock;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::hash::Hasher;
36use std::sync::atomic::{AtomicUsize, Ordering};
37use std::sync::{Arc, Mutex};
38use tracing::{debug, error, info, trace, warn};
39use xxhash_rust::xxh64::Xxh64;
40
41type BrainRegionIoRegistry = HashMap<String, (Vec<String>, Vec<String>)>;
42
43use crate::models::{BrainRegion, BrainRegionHierarchy, CorticalArea, CorticalAreaDimensions};
44use crate::types::{BduError, BduResult};
45use feagi_npu_neural::types::NeuronId;
46use feagi_structures::genomic::cortical_area::{CorticalAreaType, CorticalID, CustomCorticalType};
47use feagi_structures::genomic::descriptors::GenomeCoordinate3D;
48
49// State manager access for fatigue calculation
50// Note: feagi-state-manager is always available when std is enabled (it's a default feature)
51use feagi_state_manager::StateManager;
52
53const DATA_HASH_SEED: u64 = 0;
54// JSON number precision (Godot) is limited to 53 bits; mask to keep hashes stable across transports.
55const HASH_SAFE_MASK: u64 = (1u64 << 53) - 1;
56
57// NPU integration (optional dependency)
58// use feagi_npu_burst_engine::RustNPU; // Now using DynamicNPU
59
60/// Global singleton instance of ConnectomeManager
61static INSTANCE: Lazy<Arc<RwLock<ConnectomeManager>>> =
62    Lazy::new(|| Arc::new(RwLock::new(ConnectomeManager::new())));
63
64/// Configuration for ConnectomeManager
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ConnectomeConfig {
67    /// Maximum number of neurons (for NPU sizing)
68    pub max_neurons: usize,
69
70    /// Maximum number of synapses (for NPU sizing)
71    pub max_synapses: usize,
72
73    /// Backend type ("cpu", "cuda", "wgpu")
74    pub backend: String,
75}
76
77impl Default for ConnectomeConfig {
78    fn default() -> Self {
79        Self {
80            max_neurons: 10_000_000,
81            max_synapses: 100_000_000,
82            backend: "cpu".to_string(),
83        }
84    }
85}
86
87/// Central manager for the FEAGI connectome
88///
89/// ## Responsibilities
90///
91/// 1. **Cortical Area Management**: Add, remove, query cortical areas
92/// 2. **Brain Region Management**: Hierarchical organization
93/// 3. **Neuron/Synapse Queries**: High-level API (delegates to NPU)
94/// 4. **Genome I/O**: Load/save brain structure
95///
96/// ## Data Storage
97///
98/// - **Cortical areas**: Stored in HashMap for O(1) lookup
99/// - **Brain regions**: Stored in BrainRegionHierarchy
100/// - **Neuron data**: Lives in NPU (not stored here)
101/// - **Synapse data**: Lives in NPU (not stored here)
102///
103/// ## Thread Safety
104///
105/// Uses `RwLock` for concurrent reads with exclusive writes.
106/// Multiple threads can read simultaneously, but writes block.
107///
108pub struct ConnectomeManager {
109    /// Map of cortical_id -> CorticalArea metadata
110    cortical_areas: HashMap<CorticalID, CorticalArea>,
111
112    /// Map of cortical_id -> cortical_idx (fast reverse lookup)
113    cortical_id_to_idx: HashMap<CorticalID, u32>,
114
115    /// Map of cortical_idx -> cortical_id (fast reverse lookup)
116    cortical_idx_to_id: HashMap<u32, CorticalID>,
117
118    /// Next available cortical index
119    next_cortical_idx: u32,
120
121    /// Brain region hierarchy
122    brain_regions: BrainRegionHierarchy,
123
124    /// Morphology registry from loaded genome
125    morphology_registry: feagi_evolutionary::MorphologyRegistry,
126
127    /// Configuration
128    config: ConnectomeConfig,
129
130    /// Optional reference to the Rust NPU for neuron/synapse queries
131    ///
132    /// This is set by the Python process manager after NPU initialization.
133    /// All neuron/synapse data queries delegate to the NPU.
134    /// Wrapped in TracingMutex to automatically log all lock acquisitions
135    npu: Option<Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>,
136
137    /// Plasticity executor reference (optional, only when plasticity feature is enabled)
138    #[cfg(feature = "plasticity")]
139    plasticity_executor:
140        Option<Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>>,
141
142    /// Cached neuron count (lock-free read) - updated by burst engine
143    /// This prevents health checks from blocking on NPU lock
144    cached_neuron_count: Arc<AtomicUsize>,
145
146    /// Cached synapse count (lock-free read) - updated by burst engine
147    /// This prevents health checks from blocking on NPU lock
148    cached_synapse_count: Arc<AtomicUsize>,
149
150    /// Per-area neuron count cache (lock-free reads) - updated when neurons are created/deleted
151    /// This prevents health checks from blocking on NPU lock
152    cached_neuron_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
153
154    /// Per-area synapse count cache (lock-free reads) - updated when synapses are created/deleted
155    /// This prevents health checks from blocking on NPU lock
156    cached_synapse_counts_per_area: Arc<RwLock<HashMap<CorticalID, AtomicUsize>>>,
157
158    /// Is the connectome initialized (has cortical areas)?
159    initialized: bool,
160
161    /// Last fatigue index calculation time (for rate limiting)
162    last_fatigue_calculation: Arc<Mutex<std::time::Instant>>,
163}
164
165/// Type alias for neuron batch data: (x, y, z, threshold, threshold_limit, leak, resting, neuron_type, refractory_period, excitability, consecutive_fire_limit, snooze_period, mp_charge_accumulation)
166type NeuronData = (
167    u32,
168    u32,
169    u32,
170    f32,
171    f32,
172    f32,
173    f32,
174    i32,
175    u16,
176    f32,
177    u16,
178    u16,
179    bool,
180);
181
182impl ConnectomeManager {
183    /// Create a new ConnectomeManager (private - use `instance()`)
184    fn new() -> Self {
185        Self {
186            cortical_areas: HashMap::new(),
187            cortical_id_to_idx: HashMap::new(),
188            cortical_idx_to_id: HashMap::new(),
189            // CRITICAL: Reserve indices 0 (_death) and 1 (_power) - start regular areas at 2
190            next_cortical_idx: 3, // Reserve 0=_death, 1=_power, 2=_fatigue
191            brain_regions: BrainRegionHierarchy::new(),
192            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
193            config: ConnectomeConfig::default(),
194            npu: None,
195            #[cfg(feature = "plasticity")]
196            plasticity_executor: None,
197            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
198            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
199            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
200            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
201            initialized: false,
202            last_fatigue_calculation: Arc::new(Mutex::new(
203                std::time::Instant::now() - std::time::Duration::from_secs(10),
204            )), // Initialize to allow first calculation
205        }
206    }
207
208    /// Get the global singleton instance
209    ///
210    /// # Returns
211    ///
212    /// Arc to the ConnectomeManager wrapped in RwLock
213    ///
214    /// # Example
215    ///
216    /// ```ignore
217    /// use feagi_brain_development::ConnectomeManager;
218    ///
219    /// let manager = ConnectomeManager::instance();
220    /// let read_lock = manager.read();
221    /// let area_count = read_lock.get_cortical_area_count();
222    /// ```
223    ///
224    pub fn instance() -> Arc<RwLock<ConnectomeManager>> {
225        // Note: Singleton is always f32 for backward compatibility
226        // New code should use ConnectomeManager::<T>::new_for_testing_with_npu() for custom types
227        Arc::clone(&*INSTANCE)
228    }
229
230    /// Calculate optimal visualization voxel granularity for a cortical area
231    ///
232    /// This function determines the granularity for aggregated rendering based on:
233    /// - Total voxel count (larger areas get larger chunks)
234    /// - Aspect ratio (handles thin dimensions like 1024×900×3)
235    /// - Target chunk count (~2k-10k chunks for manageable message size)
236    ///
237    /// # Arguments
238    ///
239    /// * `dimensions` - The cortical area dimensions (width, height, depth)
240    ///
241    /// # Returns
242    ///
243    /// Tuple of (chunk_x, chunk_y, chunk_z) that divides evenly into dimensions
244    ///
245    ///
246    /// Create a new isolated instance for testing
247    ///
248    /// This bypasses the singleton pattern and creates a fresh instance.
249    /// Use this in tests to avoid conflicts between parallel test runs.
250    ///
251    /// # Example
252    ///
253    /// ```rust
254    /// let manager = ConnectomeManager::new_for_testing();
255    /// // Use manager in isolated test
256    /// ```
257    pub fn new_for_testing() -> Self {
258        Self {
259            cortical_areas: HashMap::new(),
260            cortical_id_to_idx: HashMap::new(),
261            cortical_idx_to_id: HashMap::new(),
262            next_cortical_idx: 0,
263            brain_regions: BrainRegionHierarchy::new(),
264            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
265            config: ConnectomeConfig::default(),
266            npu: None,
267            #[cfg(feature = "plasticity")]
268            plasticity_executor: None,
269            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
270            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
271            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
272            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
273            initialized: false,
274            last_fatigue_calculation: Arc::new(Mutex::new(
275                std::time::Instant::now() - std::time::Duration::from_secs(10),
276            )),
277        }
278    }
279
280    /// Create a new isolated instance for testing with NPU
281    ///
282    /// This bypasses the singleton pattern and creates a fresh instance with NPU connected.
283    /// Use this in tests to avoid conflicts between parallel test runs.
284    ///
285    /// # Arguments
286    ///
287    /// * `npu` - Arc<TracingMutex<DynamicNPU>> to connect to this manager
288    ///
289    /// # Example
290    ///
291    /// ```rust
292    /// let npu = Arc::new(TracingMutex::new(RustNPU::new(1_000_000, 10_000_000, 10), "NPU"));
293    /// let manager = ConnectomeManager::new_for_testing_with_npu(npu);
294    /// ```
295    pub fn new_for_testing_with_npu(
296        npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
297    ) -> Self {
298        Self {
299            cortical_areas: HashMap::new(),
300            cortical_id_to_idx: HashMap::new(),
301            cortical_idx_to_id: HashMap::new(),
302            next_cortical_idx: 3,
303            brain_regions: BrainRegionHierarchy::new(),
304            morphology_registry: feagi_evolutionary::MorphologyRegistry::new(),
305            config: ConnectomeConfig::default(),
306            npu: Some(npu),
307            #[cfg(feature = "plasticity")]
308            plasticity_executor: None,
309            cached_neuron_count: Arc::new(AtomicUsize::new(0)),
310            cached_synapse_count: Arc::new(AtomicUsize::new(0)),
311            cached_neuron_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
312            cached_synapse_counts_per_area: Arc::new(RwLock::new(HashMap::new())),
313            initialized: false,
314            last_fatigue_calculation: Arc::new(Mutex::new(
315                std::time::Instant::now() - std::time::Duration::from_secs(10),
316            )),
317        }
318    }
319
320    /// Set up core morphologies in the registry (for testing only)
321    ///
322    /// This is a test helper to set up core morphologies (projector, block_to_block, etc.)
323    /// in the morphology registry so that synaptogenesis tests can run.
324    ///
325    /// # Note
326    ///
327    /// This should only be called in tests. Morphologies are typically loaded from genome files.
328    /// This method is public to allow integration tests to access it.
329    pub fn setup_core_morphologies_for_testing(&mut self) {
330        feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
331    }
332
333    /// Reset the singleton (for testing only)
334    ///
335    /// # Safety
336    ///
337    /// This should only be called in tests to reset state between test runs.
338    /// Calling this in production code will cause all references to the old
339    /// instance to become stale.
340    ///
341    #[cfg(test)]
342    pub fn reset_for_testing() {
343        let mut instance = INSTANCE.write();
344        *instance = Self::new();
345    }
346
347    // ======================================================================
348    // Data Hashing (event-driven updates for health_check)
349    // ======================================================================
350
351    /// Update stored hashes for data types that have changed.
352    fn update_state_hashes(
353        &self,
354        brain_regions: Option<u64>,
355        cortical_areas: Option<u64>,
356        brain_geometry: Option<u64>,
357        morphologies: Option<u64>,
358        cortical_mappings: Option<u64>,
359    ) {
360        let state_manager = StateManager::instance();
361        let state_manager = state_manager.read();
362        if let Some(value) = brain_regions {
363            state_manager.set_brain_regions_hash(value);
364        }
365        if let Some(value) = cortical_areas {
366            state_manager.set_cortical_areas_hash(value);
367        }
368        if let Some(value) = brain_geometry {
369            state_manager.set_brain_geometry_hash(value);
370        }
371        if let Some(value) = morphologies {
372            state_manager.set_morphologies_hash(value);
373        }
374        if let Some(value) = cortical_mappings {
375            state_manager.set_cortical_mappings_hash(value);
376        }
377    }
378
379    /// Refresh the brain regions hash (hierarchy, membership, and properties).
380    fn refresh_brain_regions_hash(&self) {
381        let hash = self.compute_brain_regions_hash();
382        self.update_state_hashes(Some(hash), None, None, None, None);
383    }
384
385    #[allow(dead_code)]
386    /// Refresh the cortical areas hash (metadata and properties).
387    fn refresh_cortical_areas_hash(&self) {
388        let hash = self.compute_cortical_areas_hash();
389        self.update_state_hashes(None, Some(hash), None, None, None);
390    }
391
392    #[allow(dead_code)]
393    /// Refresh the brain geometry hash (positions, dimensions, 2D coordinates).
394    fn refresh_brain_geometry_hash(&self) {
395        let hash = self.compute_brain_geometry_hash();
396        self.update_state_hashes(None, None, Some(hash), None, None);
397    }
398
399    /// Refresh the morphologies hash.
400    fn refresh_morphologies_hash(&self) {
401        let hash = self.compute_morphologies_hash();
402        self.update_state_hashes(None, None, None, Some(hash), None);
403    }
404
405    /// Refresh the cortical mappings hash.
406    fn refresh_cortical_mappings_hash(&self) {
407        let hash = self.compute_cortical_mappings_hash();
408        self.update_state_hashes(None, None, None, None, Some(hash));
409    }
410
411    /// Refresh cortical area-related hashes based on the affected data.
412    pub fn refresh_cortical_area_hashes(&self, properties_changed: bool, geometry_changed: bool) {
413        let cortical_hash = if properties_changed {
414            Some(self.compute_cortical_areas_hash())
415        } else {
416            None
417        };
418        let geometry_hash = if geometry_changed {
419            Some(self.compute_brain_geometry_hash())
420        } else {
421            None
422        };
423        self.update_state_hashes(None, cortical_hash, geometry_hash, None, None);
424    }
425
426    /// Compute hash for brain regions (hierarchy, membership, and properties).
427    fn compute_brain_regions_hash(&self) -> u64 {
428        let mut hasher = Xxh64::new(DATA_HASH_SEED);
429        let mut region_ids: Vec<String> = self
430            .brain_regions
431            .get_all_region_ids()
432            .into_iter()
433            .cloned()
434            .collect();
435        region_ids.sort();
436
437        for region_id in region_ids {
438            let Some(region) = self.brain_regions.get_region(&region_id) else {
439                continue;
440            };
441            Self::hash_str(&mut hasher, &region_id);
442            Self::hash_str(&mut hasher, &region.name);
443            Self::hash_str(&mut hasher, &region.region_type.to_string());
444            let parent_id = self.brain_regions.get_parent(&region_id);
445            match parent_id {
446                Some(parent) => Self::hash_str(&mut hasher, parent),
447                None => Self::hash_str(&mut hasher, "null"),
448            }
449
450            let mut cortical_ids: Vec<String> = region
451                .cortical_areas
452                .iter()
453                .map(|id| id.as_base_64())
454                .collect();
455            cortical_ids.sort();
456            for cortical_id in cortical_ids {
457                Self::hash_str(&mut hasher, &cortical_id);
458            }
459
460            Self::hash_properties_filtered(&mut hasher, &region.properties, &[]);
461        }
462
463        hasher.finish() & HASH_SAFE_MASK
464    }
465
466    /// Compute hash for cortical areas and properties (excluding mappings).
467    fn compute_cortical_areas_hash(&self) -> u64 {
468        let mut hasher = Xxh64::new(DATA_HASH_SEED);
469        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
470        areas.sort_by_key(|area| area.cortical_id.as_base_64());
471
472        for area in areas {
473            let cortical_id = area.cortical_id.as_base_64();
474            Self::hash_str(&mut hasher, &cortical_id);
475            hasher.write_u32(area.cortical_idx);
476            Self::hash_str(&mut hasher, &area.name);
477            Self::hash_str(&mut hasher, &area.cortical_type.to_string());
478
479            let excluded = ["cortical_mapping_dst", "upstream_cortical_areas"];
480            Self::hash_properties_filtered(&mut hasher, &area.properties, &excluded);
481        }
482
483        hasher.finish() & HASH_SAFE_MASK
484    }
485
486    /// Compute hash for brain geometry (area positions, dimensions, and 2D coordinates).
487    fn compute_brain_geometry_hash(&self) -> u64 {
488        let mut hasher = Xxh64::new(DATA_HASH_SEED);
489        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
490        areas.sort_by_key(|area| area.cortical_id.as_base_64());
491
492        for area in areas {
493            let cortical_id = area.cortical_id.as_base_64();
494            Self::hash_str(&mut hasher, &cortical_id);
495
496            Self::hash_i32(&mut hasher, area.position.x);
497            Self::hash_i32(&mut hasher, area.position.y);
498            Self::hash_i32(&mut hasher, area.position.z);
499
500            Self::hash_u32(&mut hasher, area.dimensions.width);
501            Self::hash_u32(&mut hasher, area.dimensions.height);
502            Self::hash_u32(&mut hasher, area.dimensions.depth);
503
504            let coord_2d = area
505                .properties
506                .get("coordinate_2d")
507                .or_else(|| area.properties.get("coordinates_2d"));
508            match coord_2d {
509                Some(value) => Self::hash_json_value(&mut hasher, value),
510                None => Self::hash_str(&mut hasher, "null"),
511            }
512        }
513
514        hasher.finish() & HASH_SAFE_MASK
515    }
516
517    /// Compute hash for morphologies.
518    fn compute_morphologies_hash(&self) -> u64 {
519        let mut hasher = Xxh64::new(DATA_HASH_SEED);
520        let mut morphology_ids = self.morphology_registry.morphology_ids();
521        morphology_ids.sort();
522
523        for morphology_id in morphology_ids {
524            if let Some(morphology) = self.morphology_registry.get(&morphology_id) {
525                Self::hash_str(&mut hasher, &morphology_id);
526                Self::hash_str(&mut hasher, &format!("{:?}", morphology.morphology_type));
527                Self::hash_str(&mut hasher, &morphology.class);
528                if let Ok(value) = serde_json::to_value(&morphology.parameters) {
529                    Self::hash_json_value(&mut hasher, &value);
530                }
531            }
532        }
533
534        hasher.finish() & HASH_SAFE_MASK
535    }
536
537    /// Compute hash for cortical mappings (cortical_mapping_dst).
538    fn compute_cortical_mappings_hash(&self) -> u64 {
539        let mut hasher = Xxh64::new(DATA_HASH_SEED);
540        let mut areas: Vec<&CorticalArea> = self.cortical_areas.values().collect();
541        areas.sort_by_key(|area| area.cortical_id.as_base_64());
542
543        for area in areas {
544            let cortical_id = area.cortical_id.as_base_64();
545            Self::hash_str(&mut hasher, &cortical_id);
546            if let Some(serde_json::Value::Object(map)) =
547                area.properties.get("cortical_mapping_dst")
548            {
549                let mut dest_ids: Vec<&String> = map.keys().collect();
550                dest_ids.sort();
551                for dest_id in dest_ids {
552                    Self::hash_str(&mut hasher, dest_id);
553                    if let Some(value) = map.get(dest_id) {
554                        Self::hash_json_value(&mut hasher, value);
555                    }
556                }
557            } else {
558                Self::hash_str(&mut hasher, "null");
559            }
560        }
561
562        hasher.finish() & HASH_SAFE_MASK
563    }
564
565    /// Hash a string with a separator to avoid concatenation collisions.
566    fn hash_str(hasher: &mut Xxh64, value: &str) {
567        hasher.write(value.as_bytes());
568        hasher.write_u8(0);
569    }
570
571    /// Hash a signed 32-bit integer deterministically.
572    fn hash_i32(hasher: &mut Xxh64, value: i32) {
573        hasher.write(&value.to_le_bytes());
574    }
575
576    /// Hash an unsigned 32-bit integer deterministically.
577    fn hash_u32(hasher: &mut Xxh64, value: u32) {
578        hasher.write(&value.to_le_bytes());
579    }
580
581    /// Hash JSON values deterministically with sorted object keys.
582    fn hash_json_value(hasher: &mut Xxh64, value: &serde_json::Value) {
583        match value {
584            serde_json::Value::Null => {
585                hasher.write_u8(0);
586            }
587            serde_json::Value::Bool(val) => {
588                hasher.write_u8(1);
589                hasher.write_u8(*val as u8);
590            }
591            serde_json::Value::Number(num) => {
592                hasher.write_u8(2);
593                Self::hash_str(hasher, &num.to_string());
594            }
595            serde_json::Value::String(val) => {
596                hasher.write_u8(3);
597                Self::hash_str(hasher, val);
598            }
599            serde_json::Value::Array(items) => {
600                hasher.write_u8(4);
601                for item in items {
602                    Self::hash_json_value(hasher, item);
603                }
604            }
605            serde_json::Value::Object(map) => {
606                hasher.write_u8(5);
607                let mut keys: Vec<&String> = map.keys().collect();
608                keys.sort();
609                for key in keys {
610                    Self::hash_str(hasher, key);
611                    if let Some(val) = map.get(key) {
612                        Self::hash_json_value(hasher, val);
613                    }
614                }
615            }
616        }
617    }
618
619    /// Hash JSON properties deterministically, excluding specific keys.
620    fn hash_properties_filtered(
621        hasher: &mut Xxh64,
622        properties: &HashMap<String, serde_json::Value>,
623        excluded_keys: &[&str],
624    ) {
625        let mut keys: Vec<&String> = properties.keys().collect();
626        keys.sort();
627        for key in keys {
628            if excluded_keys.contains(&key.as_str()) {
629                continue;
630            }
631            Self::hash_str(hasher, key);
632            if let Some(value) = properties.get(key) {
633                Self::hash_json_value(hasher, value);
634            }
635        }
636    }
637
638    // ======================================================================
639    // Cortical Area Management
640    // ======================================================================
641
642    /// Add a new cortical area
643    ///
644    /// # Arguments
645    ///
646    /// * `area` - The cortical area to add
647    ///
648    /// # Returns
649    ///
650    /// The assigned cortical index
651    ///
652    /// # Errors
653    ///
654    /// Returns error if:
655    /// - An area with the same cortical_id already exists
656    /// - The area's cortical_idx conflicts with an existing area
657    ///
658    pub fn add_cortical_area(&mut self, mut area: CorticalArea) -> BduResult<u32> {
659        // Check if area already exists
660        if self.cortical_areas.contains_key(&area.cortical_id) {
661            return Err(BduError::InvalidArea(format!(
662                "Cortical area {} already exists",
663                area.cortical_id
664            )));
665        }
666
667        // CRITICAL: Reserve cortical_idx 0 for _death, 1 for _power, 2 for _fatigue
668        // Use feagi-data-processing types as single source of truth
669        use feagi_structures::genomic::cortical_area::CoreCorticalType;
670
671        let death_id = CoreCorticalType::Death.to_cortical_id();
672        let power_id = CoreCorticalType::Power.to_cortical_id();
673        let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
674
675        let is_death_area = area.cortical_id == death_id;
676        let is_power_area = area.cortical_id == power_id;
677        let is_fatigue_area = area.cortical_id == fatigue_id;
678
679        if is_death_area {
680            trace!(
681                target: "feagi-bdu",
682                "[CORE-AREA] Assigning RESERVED cortical_idx=0 to _death area (id={})",
683                area.cortical_id
684            );
685            area.cortical_idx = 0;
686        } else if is_power_area {
687            trace!(
688                target: "feagi-bdu",
689                "[CORE-AREA] Assigning RESERVED cortical_idx=1 to _power area (id={})",
690                area.cortical_id
691            );
692            area.cortical_idx = 1;
693        } else if is_fatigue_area {
694            trace!(
695                target: "feagi-bdu",
696                "[CORE-AREA] Assigning RESERVED cortical_idx=2 to _fatigue area (id={})",
697                area.cortical_id
698            );
699            area.cortical_idx = 2;
700        } else {
701            // Regular areas: assign cortical_idx if not set (will be ≥3 due to next_cortical_idx=3 initialization)
702            if area.cortical_idx == 0 {
703                area.cortical_idx = self.next_cortical_idx;
704                self.next_cortical_idx += 1;
705                trace!(
706                    target: "feagi-bdu",
707                    "[REGULAR-AREA] Assigned cortical_idx={} to area '{}' (should be ≥3)",
708                    area.cortical_idx,
709                    area.cortical_id.as_base_64()
710                );
711            } else {
712                // Check for reserved index collision
713                if area.cortical_idx == 0 || area.cortical_idx == 1 || area.cortical_idx == 2 {
714                    warn!(
715                        "Regular area '{}' attempted to use RESERVED cortical_idx={}! Reassigning to next available.",
716                        area.cortical_id, area.cortical_idx);
717                    area.cortical_idx = self.next_cortical_idx;
718                    self.next_cortical_idx += 1;
719                    info!(
720                        "   Reassigned '{}' to cortical_idx={}",
721                        area.cortical_id, area.cortical_idx
722                    );
723                } else if self.cortical_idx_to_id.contains_key(&area.cortical_idx) {
724                    return Err(BduError::InvalidArea(format!(
725                        "Cortical index {} is already in use",
726                        area.cortical_idx
727                    )));
728                }
729
730                // Update next_cortical_idx if needed
731                if area.cortical_idx >= self.next_cortical_idx {
732                    self.next_cortical_idx = area.cortical_idx + 1;
733                }
734            }
735        }
736
737        let cortical_id = area.cortical_id;
738        let cortical_idx = area.cortical_idx;
739
740        // Update lookup maps
741        self.cortical_id_to_idx.insert(cortical_id, cortical_idx);
742        self.cortical_idx_to_id.insert(cortical_idx, cortical_id);
743
744        // Initialize upstream_cortical_areas property (empty array for O(1) lookup)
745        area.properties
746            .insert("upstream_cortical_areas".to_string(), serde_json::json!([]));
747
748        // Default visualization voxel granularity is 1x1x1 (assumed, not stored)
749        // User overrides are stored in properties["visualization_voxel_granularity"] only if != 1x1x1
750
751        // If the caller provided a parent brain region ID, persist the association in the
752        // BrainRegionHierarchy membership set (this drives /v1/region/regions_members).
753        //
754        // IMPORTANT: This is separate from storing "parent_region_id" in the cortical area's
755        // properties. BV may show that property even if the hierarchy isn't updated.
756        let parent_region_id = area
757            .properties
758            .get("parent_region_id")
759            .and_then(|v| v.as_str())
760            .map(|s| s.to_string());
761
762        // Store area
763        self.cortical_areas.insert(cortical_id, area);
764
765        // Update region membership (source of truth for region->areas listing)
766        if let Some(region_id) = parent_region_id {
767            let region = self
768                .brain_regions
769                .get_region_mut(&region_id)
770                .ok_or_else(|| {
771                    BduError::InvalidArea(format!(
772                        "Unknown parent_region_id '{}' for cortical area {}",
773                        region_id,
774                        cortical_id.as_base_64()
775                    ))
776                })?;
777            region.add_area(cortical_id);
778        }
779
780        // CRITICAL: Initialize per-area count caches to 0 (lock-free for readers)
781        // This allows healthcheck endpoints to read counts without NPU lock
782        {
783            let mut neuron_cache = self.cached_neuron_counts_per_area.write();
784            neuron_cache.insert(cortical_id, AtomicUsize::new(0));
785            let mut synapse_cache = self.cached_synapse_counts_per_area.write();
786            synapse_cache.insert(cortical_id, AtomicUsize::new(0));
787        }
788        // @cursor:critical-path - BV pulls per-area stats from StateManager without NPU lock.
789        if let Some(state_manager) = StateManager::instance().try_read() {
790            state_manager.init_cortical_area_stats(&cortical_id.as_base_64());
791        }
792
793        // CRITICAL: Register cortical area in NPU during corticogenesis
794        // This must happen BEFORE neurogenesis so neurons can look up their cortical IDs
795        // Use base64 format for proper CorticalID conversion
796        if let Some(ref npu) = self.npu {
797            trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: attempting NPU lock for registration");
798            if let Ok(mut npu_lock) = npu.lock() {
799                trace!(target: "feagi-bdu", "[LOCK-TRACE] add_cortical_area: acquired NPU lock for registration");
800                npu_lock.register_cortical_area(cortical_idx, cortical_id.as_base_64());
801                trace!(
802                    target: "feagi-bdu",
803                    "Registered cortical area idx={} -> '{}' in NPU",
804                    cortical_idx,
805                    cortical_id.as_base_64()
806                );
807            }
808        }
809
810        // Synchronize cortical area flags with NPU (psp_uniform_distribution, mp_driven_psp, etc.)
811        self.sync_cortical_area_flags_to_npu()?;
812
813        self.initialized = true;
814
815        self.refresh_cortical_area_hashes(true, true);
816        self.refresh_brain_regions_hash();
817
818        Ok(cortical_idx)
819    }
820
821    /// Remove a cortical area by ID
822    ///
823    /// # Arguments
824    ///
825    /// * `cortical_id` - ID of the cortical area to remove
826    ///
827    /// # Returns
828    ///
829    /// `Ok(())` if removed, error if area doesn't exist
830    ///
831    /// # Note
832    ///
833    /// This does NOT remove neurons from the NPU - that must be done separately.
834    ///
835    pub fn remove_cortical_area(&mut self, cortical_id: &CorticalID) -> BduResult<()> {
836        let area = self.cortical_areas.remove(cortical_id).ok_or_else(|| {
837            BduError::InvalidArea(format!("Cortical area {} does not exist", cortical_id))
838        })?;
839
840        // Remove from lookup maps
841        self.cortical_id_to_idx.remove(cortical_id);
842        self.cortical_idx_to_id.remove(&area.cortical_idx);
843
844        self.refresh_cortical_area_hashes(true, true);
845        Ok(())
846    }
847
848    /// Update a cortical area ID without changing its cortical_idx.
849    ///
850    /// This remaps internal lookup tables, brain-region membership, and mapping keys.
851    pub fn rename_cortical_area_id(
852        &mut self,
853        old_id: &CorticalID,
854        new_id: CorticalID,
855        new_cortical_type: CorticalAreaType,
856    ) -> BduResult<()> {
857        self.rename_cortical_area_id_with_options(old_id, new_id, new_cortical_type, true)
858    }
859
860    /// Update a cortical area ID without changing its cortical_idx, with optional NPU registry update.
861    pub fn rename_cortical_area_id_with_options(
862        &mut self,
863        old_id: &CorticalID,
864        new_id: CorticalID,
865        new_cortical_type: CorticalAreaType,
866        update_npu_registry: bool,
867    ) -> BduResult<()> {
868        if !self.cortical_areas.contains_key(old_id) {
869            return Err(BduError::InvalidArea(format!(
870                "Cortical area {} does not exist",
871                old_id
872            )));
873        }
874        if self.cortical_areas.contains_key(&new_id) {
875            return Err(BduError::InvalidArea(format!(
876                "Cortical area {} already exists",
877                new_id
878            )));
879        }
880
881        let mut area = self.cortical_areas.remove(old_id).ok_or_else(|| {
882            BduError::InvalidArea(format!("Cortical area {} does not exist", old_id))
883        })?;
884        let cortical_idx = area.cortical_idx;
885        area.cortical_id = new_id;
886        area.cortical_type = new_cortical_type;
887
888        self.cortical_areas.insert(new_id, area);
889        self.cortical_id_to_idx.remove(old_id);
890        self.cortical_id_to_idx.insert(new_id, cortical_idx);
891        self.cortical_idx_to_id.insert(cortical_idx, new_id);
892
893        // Update per-area count caches
894        {
895            let mut neuron_cache = self.cached_neuron_counts_per_area.write();
896            if let Some(value) = neuron_cache.remove(old_id) {
897                neuron_cache.insert(new_id, value);
898            }
899            let mut synapse_cache = self.cached_synapse_counts_per_area.write();
900            if let Some(value) = synapse_cache.remove(old_id) {
901                synapse_cache.insert(new_id, value);
902            }
903        }
904
905        // Update brain-region membership
906        self.brain_regions.rename_cortical_area_id(old_id, new_id);
907
908        // Update cortical mapping properties referencing the old ID
909        let old_id_str = old_id.as_base_64();
910        let new_id_str = new_id.as_base_64();
911        for area in self.cortical_areas.values_mut() {
912            if let Some(mapping) = area
913                .properties
914                .get_mut("cortical_mapping_dst")
915                .and_then(|v| v.as_object_mut())
916            {
917                if let Some(value) = mapping.remove(&old_id_str) {
918                    mapping.insert(new_id_str.clone(), value);
919                }
920            }
921        }
922
923        // Update NPU cortical_id registry if requested
924        if update_npu_registry {
925            if let Some(ref npu) = self.npu {
926                if let Ok(mut npu_lock) = npu.lock() {
927                    npu_lock.register_cortical_area(cortical_idx, new_id.as_base_64());
928                }
929            }
930        }
931
932        self.refresh_cortical_area_hashes(true, true);
933        self.refresh_brain_regions_hash();
934        self.refresh_cortical_mappings_hash();
935
936        Ok(())
937    }
938
939    /// Get a cortical area by ID
940    pub fn get_cortical_area(&self, cortical_id: &CorticalID) -> Option<&CorticalArea> {
941        self.cortical_areas.get(cortical_id)
942    }
943
944    /// Get a mutable reference to a cortical area
945    pub fn get_cortical_area_mut(&mut self, cortical_id: &CorticalID) -> Option<&mut CorticalArea> {
946        self.cortical_areas.get_mut(cortical_id)
947    }
948
949    /// Get cortical index by ID
950    pub fn get_cortical_idx(&self, cortical_id: &CorticalID) -> Option<u32> {
951        self.cortical_id_to_idx.get(cortical_id).copied()
952    }
953
954    /// Find which brain region contains a cortical area
955    ///
956    /// This is used to populate `parent_region_id` in API responses for Brain Visualizer.
957    /// Delegates to BrainRegionHierarchy for the actual search.
958    ///
959    /// # Arguments
960    /// * `cortical_id` - Cortical area to search for
961    ///
962    /// # Returns
963    /// * `Option<String>` - Parent region ID (UUID string) if found
964    ///
965    pub fn get_parent_region_id_for_area(&self, cortical_id: &CorticalID) -> Option<String> {
966        self.brain_regions.find_region_containing_area(cortical_id)
967    }
968
969    /// Recompute brain-region `inputs`/`outputs` registries from current cortical mappings.
970    ///
971    /// This drives `/v1/region/regions_members` (via `BrainRegion.properties["inputs"/"outputs"]`).
972    ///
973    /// Semantics (matches Python `_auto_assign_region_io()` and BV expectations):
974    /// - **outputs**: Any cortical area *in the region* that connects to an area outside the region
975    /// - **inputs**: Any cortical area *in the region* that receives a connection from outside the region
976    ///
977    /// This function updates the hierarchy in-place and returns the computed base64 ID lists
978    /// for downstream persistence into `RuntimeGenome`.
979    pub fn recompute_brain_region_io_registry(&mut self) -> BduResult<BrainRegionIoRegistry> {
980        use std::collections::HashSet;
981
982        let region_ids: Vec<String> = self
983            .brain_regions
984            .get_all_region_ids()
985            .into_iter()
986            .cloned()
987            .collect();
988
989        let mut inputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
990        let mut outputs_by_region: HashMap<String, HashSet<String>> = HashMap::new();
991
992        // Initialize so regions with no IO still get cleared deterministically.
993        for rid in &region_ids {
994            inputs_by_region.insert(rid.clone(), HashSet::new());
995            outputs_by_region.insert(rid.clone(), HashSet::new());
996        }
997
998        for (src_id, src_area) in &self.cortical_areas {
999            let Some(dstmap) = src_area
1000                .properties
1001                .get("cortical_mapping_dst")
1002                .and_then(|v| v.as_object())
1003            else {
1004                continue;
1005            };
1006
1007            let Some(src_region_id) = self.brain_regions.find_region_containing_area(src_id) else {
1008                warn!(
1009                    target: "feagi-bdu",
1010                    "Skipping region IO for source area {} (not in any region)",
1011                    src_id.as_base_64()
1012                );
1013                continue;
1014            };
1015
1016            for dst_id_str in dstmap.keys() {
1017                let dst_id = CorticalID::try_from_base_64(dst_id_str).map_err(|e| {
1018                    BduError::InvalidArea(format!(
1019                        "Unable to recompute region IO: invalid destination cortical id '{}' in cortical_mapping_dst for {}: {}",
1020                        dst_id_str,
1021                        src_id.as_base_64(),
1022                        e
1023                    ))
1024                })?;
1025
1026                let Some(dst_region_id) = self.brain_regions.find_region_containing_area(&dst_id)
1027                else {
1028                    warn!(
1029                        target: "feagi-bdu",
1030                        "Skipping region IO for destination area {} (not in any region)",
1031                        dst_id.as_base_64()
1032                    );
1033                    continue;
1034                };
1035
1036                if src_region_id == dst_region_id {
1037                    continue;
1038                }
1039
1040                outputs_by_region
1041                    .entry(src_region_id.clone())
1042                    .or_default()
1043                    .insert(src_id.as_base_64());
1044                inputs_by_region
1045                    .entry(dst_region_id.clone())
1046                    .or_default()
1047                    .insert(dst_id.as_base_64());
1048            }
1049        }
1050
1051        let mut computed: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();
1052        for rid in region_ids {
1053            let mut inputs: Vec<String> = inputs_by_region
1054                .remove(&rid)
1055                .unwrap_or_default()
1056                .into_iter()
1057                .collect();
1058            let mut outputs: Vec<String> = outputs_by_region
1059                .remove(&rid)
1060                .unwrap_or_default()
1061                .into_iter()
1062                .collect();
1063
1064            inputs.sort();
1065            outputs.sort();
1066
1067            let region = self.brain_regions.get_region_mut(&rid).ok_or_else(|| {
1068                BduError::InvalidArea(format!(
1069                    "Unable to recompute region IO: region '{}' not found in hierarchy",
1070                    rid
1071                ))
1072            })?;
1073
1074            if inputs.is_empty() {
1075                region.properties.remove("inputs");
1076            } else {
1077                region
1078                    .properties
1079                    .insert("inputs".to_string(), serde_json::json!(inputs.clone()));
1080            }
1081
1082            if outputs.is_empty() {
1083                region.properties.remove("outputs");
1084            } else {
1085                region
1086                    .properties
1087                    .insert("outputs".to_string(), serde_json::json!(outputs.clone()));
1088            }
1089
1090            computed.insert(rid, (inputs, outputs));
1091        }
1092
1093        self.refresh_brain_regions_hash();
1094
1095        Ok(computed)
1096    }
1097
1098    /// Get the root brain region ID (region with no parent)
1099    ///
1100    /// # Returns
1101    /// * `Option<String>` - Root region ID (UUID string) if found
1102    ///
1103    pub fn get_root_region_id(&self) -> Option<String> {
1104        self.brain_regions.get_root_region_id()
1105    }
1106
1107    /// Get cortical ID by index
1108    pub fn get_cortical_id(&self, cortical_idx: u32) -> Option<&CorticalID> {
1109        self.cortical_idx_to_id.get(&cortical_idx)
1110    }
1111
1112    /// Get all cortical_idx -> cortical_id mappings (for burst loop caching)
1113    /// Returns a HashMap of cortical_idx -> cortical_id (base64 string)
1114    pub fn get_all_cortical_idx_to_id_mappings(&self) -> ahash::AHashMap<u32, String> {
1115        self.cortical_idx_to_id
1116            .iter()
1117            .map(|(idx, id)| (*idx, id.as_base_64()))
1118            .collect()
1119    }
1120
1121    /// Get all cortical_idx -> visualization_voxel_granularity mappings
1122    ///
1123    /// Returns a map of cortical_idx to (granularity_x, granularity_y, granularity_z) for areas that have
1124    /// visualization voxel granularity configured.
1125    pub fn get_all_visualization_granularities(&self) -> ahash::AHashMap<u32, (u32, u32, u32)> {
1126        let mut granularities = ahash::AHashMap::new();
1127        for (cortical_id, area) in &self.cortical_areas {
1128            let cortical_idx = self
1129                .cortical_id_to_idx
1130                .get(cortical_id)
1131                .copied()
1132                .unwrap_or(0);
1133
1134            // Extract visualization granularity overrides from properties.
1135            // Default is 1x1x1 (assumed, not stored) so we only include non-default overrides.
1136            if let Some(granularity_json) = area.properties.get("visualization_voxel_granularity") {
1137                if let Some(arr) = granularity_json.as_array() {
1138                    if arr.len() == 3 {
1139                        let x_opt = arr[0]
1140                            .as_u64()
1141                            .or_else(|| arr[0].as_f64().map(|f| f as u64));
1142                        let y_opt = arr[1]
1143                            .as_u64()
1144                            .or_else(|| arr[1].as_f64().map(|f| f as u64));
1145                        let z_opt = arr[2]
1146                            .as_u64()
1147                            .or_else(|| arr[2].as_f64().map(|f| f as u64));
1148
1149                        if let (Some(x), Some(y), Some(z)) = (x_opt, y_opt, z_opt) {
1150                            let granularity = (x as u32, y as u32, z as u32);
1151                            // Only include overrides (non-default)
1152                            if granularity != (1, 1, 1) {
1153                                granularities.insert(cortical_idx, granularity);
1154                            }
1155                        }
1156                    }
1157                }
1158            }
1159        }
1160        granularities
1161    }
1162
1163    /// Get all cortical area IDs
1164    pub fn get_cortical_area_ids(&self) -> Vec<&CorticalID> {
1165        self.cortical_areas.keys().collect()
1166    }
1167
1168    /// Get the number of cortical areas
1169    pub fn get_cortical_area_count(&self) -> usize {
1170        self.cortical_areas.len()
1171    }
1172
1173    /// Get all cortical areas that have synapses targeting the specified area (upstream/afferent areas)
1174    ///
1175    /// Reads from the `upstream_cortical_areas` property stored on the cortical area.
1176    /// This property is maintained by `add_upstream_area()` and `remove_upstream_area()`.
1177    ///
1178    /// # Arguments
1179    ///
1180    /// * `target_cortical_id` - The cortical area ID to find upstream connections for
1181    ///
1182    /// # Returns
1183    ///
1184    /// Vec of cortical_idx values for all upstream areas
1185    ///
1186    pub fn get_upstream_cortical_areas(&self, target_cortical_id: &CorticalID) -> Vec<u32> {
1187        if let Some(area) = self.cortical_areas.get(target_cortical_id) {
1188            if let Some(upstream_prop) = area.properties.get("upstream_cortical_areas") {
1189                if let Some(upstream_array) = upstream_prop.as_array() {
1190                    return upstream_array
1191                        .iter()
1192                        .filter_map(|v| v.as_u64().map(|n| n as u32))
1193                        .collect();
1194                }
1195            }
1196
1197            // Property missing - data integrity issue
1198            warn!(target: "feagi-bdu",
1199                "Cortical area '{}' missing 'upstream_cortical_areas' property - treating as empty",
1200                target_cortical_id.as_base_64()
1201            );
1202        }
1203
1204        Vec::new()
1205    }
1206
1207    /// Filter upstream cortical indices to exclude memory areas.
1208    pub fn filter_non_memory_upstream_areas(&self, upstream: &[u32]) -> Vec<u32> {
1209        upstream
1210            .iter()
1211            .filter_map(|idx| {
1212                let cortical_id = self.cortical_idx_to_id.get(idx)?;
1213                let area = self.cortical_areas.get(cortical_id)?;
1214                if matches!(area.cortical_type, CorticalAreaType::Memory(_)) {
1215                    None
1216                } else {
1217                    Some(*idx)
1218                }
1219            })
1220            .collect()
1221    }
1222
1223    /// Recompute and persist upstream_cortical_areas for a target area from mapping properties.
1224    ///
1225    /// This is a recovery path for stale upstream tracking (e.g., bidirectional mappings
1226    /// that were mirrored into properties but not fully regenerated).
1227    pub fn refresh_upstream_cortical_areas_from_mappings(
1228        &mut self,
1229        target_cortical_id: &CorticalID,
1230    ) -> Vec<u32> {
1231        use std::collections::HashSet;
1232        let target_id_str = target_cortical_id.as_base_64();
1233        let mut upstream_idxs = HashSet::new();
1234        for (src_id, src_area) in &self.cortical_areas {
1235            if src_id == target_cortical_id {
1236                continue;
1237            }
1238            if let Some(mapping) = src_area
1239                .properties
1240                .get("cortical_mapping_dst")
1241                .and_then(|v| v.as_object())
1242            {
1243                if mapping.contains_key(&target_id_str) {
1244                    upstream_idxs.insert(src_area.cortical_idx);
1245                }
1246            }
1247        }
1248
1249        let mut upstream_list: Vec<u32> = upstream_idxs.into_iter().collect();
1250        upstream_list.sort_unstable();
1251
1252        if let Some(target_area) = self.cortical_areas.get_mut(target_cortical_id) {
1253            target_area.properties.insert(
1254                "upstream_cortical_areas".to_string(),
1255                serde_json::json!(upstream_list),
1256            );
1257        }
1258
1259        self.get_upstream_cortical_areas(target_cortical_id)
1260    }
1261
1262    /// Add an upstream cortical area to a target area's upstream list
1263    ///
1264    /// This should be called when synapses are created from src_cortical_idx to target_cortical_id.
1265    ///
1266    /// # Arguments
1267    ///
1268    /// * `target_cortical_id` - The cortical area receiving connections
1269    /// * `src_cortical_idx` - The cortical index of the source area
1270    ///
1271    pub fn add_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1272        if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1273            let upstream_array = area
1274                .properties
1275                .entry("upstream_cortical_areas".to_string())
1276                .or_insert_with(|| serde_json::json!([]));
1277
1278            if let Some(arr) = upstream_array.as_array_mut() {
1279                let src_value = serde_json::json!(src_cortical_idx);
1280                if !arr.contains(&src_value) {
1281                    arr.push(src_value);
1282                    info!(target: "feagi-bdu",
1283                        "✓ Added upstream area idx={} to cortical area '{}'",
1284                        src_cortical_idx, target_cortical_id.as_base_64()
1285                    );
1286                }
1287            }
1288        }
1289    }
1290
1291    /// Get the memory twin cortical ID for a given memory area and upstream area.
1292    pub fn get_memory_twin_for_upstream_idx(
1293        &self,
1294        memory_area_idx: u32,
1295        upstream_idx: u32,
1296    ) -> Option<CorticalID> {
1297        let memory_id = self.cortical_idx_to_id.get(&memory_area_idx)?;
1298        let upstream_id = self.cortical_idx_to_id.get(&upstream_idx)?;
1299        let area = self.cortical_areas.get(memory_id)?;
1300        let mapping = area
1301            .properties
1302            .get("memory_twin_areas")
1303            .and_then(|v| v.as_object())?;
1304        let twin_b64 = mapping.get(&upstream_id.as_base_64())?.as_str()?;
1305        CorticalID::try_from_base_64(twin_b64).ok()
1306    }
1307
1308    /// Ensure a memory twin area exists for the given upstream and memory areas.
1309    pub fn ensure_memory_twin_area(
1310        &mut self,
1311        memory_area_id: &CorticalID,
1312        upstream_area_id: &CorticalID,
1313    ) -> BduResult<CorticalID> {
1314        use crate::models::CorticalAreaExt;
1315
1316        let register_replay_mapping = |manager: &mut ConnectomeManager,
1317                                       twin_id: &CorticalID|
1318         -> BduResult<()> {
1319            let Some(npu) = manager.npu.as_ref() else {
1320                return Ok(());
1321            };
1322            let memory_area_idx =
1323                *manager
1324                    .cortical_id_to_idx
1325                    .get(memory_area_id)
1326                    .ok_or_else(|| {
1327                        BduError::InvalidArea(format!(
1328                            "Memory area idx missing for {}",
1329                            memory_area_id.as_base_64()
1330                        ))
1331                    })?;
1332            let upstream_area_idx = *manager
1333                .cortical_id_to_idx
1334                .get(upstream_area_id)
1335                .ok_or_else(|| {
1336                    BduError::InvalidArea(format!(
1337                        "Upstream area idx missing for {}",
1338                        upstream_area_id.as_base_64()
1339                    ))
1340                })?;
1341            let twin_area_idx = *manager.cortical_id_to_idx.get(twin_id).ok_or_else(|| {
1342                BduError::InvalidArea(format!(
1343                    "Twin area idx missing for {}",
1344                    twin_id.as_base_64()
1345                ))
1346            })?;
1347            let twin_area = manager.cortical_areas.get(twin_id).ok_or_else(|| {
1348                BduError::InvalidArea(format!("Twin area {} not found", twin_id.as_base_64()))
1349            })?;
1350            let potential = twin_area.firing_threshold() + twin_area.firing_threshold_increment();
1351            if let Ok(mut npu_lock) = npu.lock() {
1352                npu_lock.register_memory_twin_mapping(
1353                    memory_area_idx,
1354                    upstream_area_idx,
1355                    twin_area_idx,
1356                    potential,
1357                );
1358            }
1359            Ok(())
1360        };
1361
1362        let memory_area = self.cortical_areas.get(memory_area_id).ok_or_else(|| {
1363            BduError::InvalidArea(format!(
1364                "Memory area {} not found",
1365                memory_area_id.as_base_64()
1366            ))
1367        })?;
1368        let upstream_area = self.cortical_areas.get(upstream_area_id).ok_or_else(|| {
1369            BduError::InvalidArea(format!(
1370                "Upstream area {} not found",
1371                upstream_area_id.as_base_64()
1372            ))
1373        })?;
1374
1375        if matches!(upstream_area.cortical_type, CorticalAreaType::Memory(_)) {
1376            return Err(BduError::InvalidArea(format!(
1377                "Upstream area {} is memory type; twin creation is only for non-memory areas",
1378                upstream_area_id.as_base_64()
1379            )));
1380        }
1381
1382        if let Some(existing) = memory_area
1383            .properties
1384            .get("memory_twin_areas")
1385            .and_then(|v| v.as_object())
1386            .and_then(|map| map.get(&upstream_area_id.as_base_64()))
1387            .and_then(|v| v.as_str())
1388            .and_then(|s| CorticalID::try_from_base_64(s).ok())
1389        {
1390            self.ensure_memory_replay_mapping(memory_area_id, &existing)?;
1391            register_replay_mapping(self, &existing)?;
1392            self.refresh_cortical_mappings_hash();
1393            return Ok(existing);
1394        }
1395
1396        let twin_id = self.build_memory_twin_id(memory_area_id, upstream_area_id)?;
1397        if self.cortical_areas.contains_key(&twin_id) {
1398            if let Some(existing) = self.cortical_areas.get_mut(&twin_id) {
1399                let expected_source = upstream_area_id.as_base_64();
1400                let expected_target = memory_area_id.as_base_64();
1401                let existing_source = existing
1402                    .properties
1403                    .get("memory_twin_of")
1404                    .and_then(|v| v.as_str());
1405                let existing_target = existing
1406                    .properties
1407                    .get("memory_twin_for")
1408                    .and_then(|v| v.as_str());
1409                if existing_source != Some(expected_source.as_str())
1410                    || existing_target != Some(expected_target.as_str())
1411                {
1412                    warn!(
1413                        target: "feagi-bdu",
1414                        "Twin cortical ID properties missing/mismatched for {} -> {}; repairing",
1415                        upstream_area_id.as_base_64(),
1416                        memory_area_id.as_base_64()
1417                    );
1418                    existing.properties.insert(
1419                        "memory_twin_of".to_string(),
1420                        serde_json::json!(expected_source),
1421                    );
1422                    existing.properties.insert(
1423                        "memory_twin_for".to_string(),
1424                        serde_json::json!(expected_target),
1425                    );
1426                }
1427            }
1428            self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1429            self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1430            register_replay_mapping(self, &twin_id)?;
1431            self.refresh_cortical_mappings_hash();
1432            return Ok(twin_id);
1433        }
1434
1435        let twin_name = format!("{}_twin", upstream_area.name.replace(' ', "_"));
1436        let twin_type = CorticalAreaType::Custom(CustomCorticalType::LeakyIntegrateFire);
1437        let twin_position = self.build_memory_twin_position(memory_area, upstream_area);
1438        let mut twin_area = CorticalArea::new(
1439            twin_id,
1440            0,
1441            twin_name,
1442            upstream_area.dimensions,
1443            twin_position,
1444            twin_type,
1445        )?;
1446        twin_area.properties = self.build_memory_twin_properties(
1447            memory_area,
1448            upstream_area,
1449            memory_area_id,
1450            upstream_area_id,
1451        );
1452
1453        let _twin_idx = self.add_cortical_area(twin_area)?;
1454        let _ = self.create_neurons_for_area(&twin_id);
1455
1456        self.set_memory_twin_mapping(memory_area_id, upstream_area_id, &twin_id);
1457        self.ensure_memory_replay_mapping(memory_area_id, &twin_id)?;
1458        register_replay_mapping(self, &twin_id)?;
1459        self.refresh_cortical_mappings_hash();
1460        Ok(twin_id)
1461    }
1462
1463    fn build_memory_twin_position(
1464        &self,
1465        memory_area: &CorticalArea,
1466        upstream_area: &CorticalArea,
1467    ) -> GenomeCoordinate3D {
1468        let memory_parent = memory_area
1469            .properties
1470            .get("parent_region_id")
1471            .and_then(|v| v.as_str());
1472        let upstream_parent = upstream_area
1473            .properties
1474            .get("parent_region_id")
1475            .and_then(|v| v.as_str());
1476        let same_region = memory_parent.is_some() && memory_parent == upstream_parent;
1477
1478        if !same_region {
1479            return GenomeCoordinate3D::new(
1480                memory_area.position.x + 20,
1481                memory_area.position.y,
1482                memory_area.position.z,
1483            );
1484        }
1485
1486        let width = upstream_area.dimensions.width as f32;
1487        let margin = (width * 0.25).ceil() as i32;
1488        let offset = upstream_area.dimensions.width as i32 + margin;
1489        GenomeCoordinate3D::new(
1490            upstream_area.position.x + offset,
1491            upstream_area.position.y,
1492            upstream_area.position.z,
1493        )
1494    }
1495
1496    fn build_memory_twin_id(
1497        &self,
1498        memory_area_id: &CorticalID,
1499        upstream_area_id: &CorticalID,
1500    ) -> BduResult<CorticalID> {
1501        let mut hasher = Xxh64::new(DATA_HASH_SEED);
1502        hasher.write(memory_area_id.as_base_64().as_bytes());
1503        hasher.write(upstream_area_id.as_base_64().as_bytes());
1504        hasher.write(b"memory_twin");
1505        let hash = hasher.finish();
1506        let mut bytes = hash.to_be_bytes();
1507        bytes[0] = b'c';
1508        CorticalID::try_from_bytes(&bytes)
1509            .map_err(|e| BduError::Internal(format!("Failed to build twin cortical ID: {}", e)))
1510    }
1511
1512    fn build_memory_twin_properties(
1513        &self,
1514        memory_area: &CorticalArea,
1515        upstream_area: &CorticalArea,
1516        memory_area_id: &CorticalID,
1517        upstream_area_id: &CorticalID,
1518    ) -> HashMap<String, serde_json::Value> {
1519        let mut props = upstream_area.properties.clone();
1520        props.remove("cortical_mapping_dst");
1521        props.remove("upstream_cortical_areas");
1522        props.remove("parent_region_id");
1523        props.insert("cortical_group".to_string(), serde_json::json!("CUSTOM"));
1524        props.insert("is_mem_type".to_string(), serde_json::json!(false));
1525        props.insert(
1526            "memory_twin_of".to_string(),
1527            serde_json::json!(upstream_area_id.as_base_64()),
1528        );
1529        props.insert(
1530            "memory_twin_for".to_string(),
1531            serde_json::json!(memory_area_id.as_base_64()),
1532        );
1533        if let Some(parent_region_id) = memory_area
1534            .properties
1535            .get("parent_region_id")
1536            .and_then(|v| v.as_str())
1537        {
1538            props.insert(
1539                "parent_region_id".to_string(),
1540                serde_json::json!(parent_region_id),
1541            );
1542        }
1543        props
1544    }
1545
1546    fn set_memory_twin_mapping(
1547        &mut self,
1548        memory_area_id: &CorticalID,
1549        upstream_area_id: &CorticalID,
1550        twin_id: &CorticalID,
1551    ) {
1552        if let Some(memory_area) = self.cortical_areas.get_mut(memory_area_id) {
1553            let mapping = memory_area
1554                .properties
1555                .entry("memory_twin_areas".to_string())
1556                .or_insert_with(|| serde_json::json!({}));
1557            if let Some(map) = mapping.as_object_mut() {
1558                map.insert(
1559                    upstream_area_id.as_base_64(),
1560                    serde_json::json!(twin_id.as_base_64()),
1561                );
1562            }
1563        }
1564    }
1565
1566    fn ensure_memory_replay_mapping(
1567        &mut self,
1568        memory_area_id: &CorticalID,
1569        twin_id: &CorticalID,
1570    ) -> BduResult<()> {
1571        if !self.morphology_registry.contains("memory_replay") {
1572            feagi_evolutionary::add_core_morphologies(&mut self.morphology_registry);
1573        }
1574        self.refresh_morphologies_hash();
1575        let mapping_data = vec![serde_json::json!({
1576            "morphology_id": "memory_replay",
1577            "morphology_scalar": [1, 1, 1],
1578            "postSynapticCurrent_multiplier": 1,
1579            "plasticity_flag": false,
1580            "plasticity_constant": 0,
1581            "ltp_multiplier": 0,
1582            "ltd_multiplier": 0,
1583            "plasticity_window": 0,
1584        })];
1585        self.update_cortical_mapping(memory_area_id, twin_id, mapping_data)?;
1586        let _ = self.regenerate_synapses_for_mapping(memory_area_id, twin_id)?;
1587        // Update cortical area hash so BV refreshes area details (outgoing mappings).
1588        self.refresh_cortical_area_hashes(true, false);
1589        Ok(())
1590    }
1591
1592    /// Remove an upstream cortical area from a target area's upstream list
1593    ///
1594    /// This should be called when all synapses from src_cortical_idx to target_cortical_id are deleted.
1595    ///
1596    /// # Arguments
1597    ///
1598    /// * `target_cortical_id` - The cortical area that had connections
1599    /// * `src_cortical_idx` - The cortical index of the source area to remove
1600    ///
1601    pub fn remove_upstream_area(&mut self, target_cortical_id: &CorticalID, src_cortical_idx: u32) {
1602        if let Some(area) = self.cortical_areas.get_mut(target_cortical_id) {
1603            if let Some(upstream_prop) = area.properties.get_mut("upstream_cortical_areas") {
1604                if let Some(arr) = upstream_prop.as_array_mut() {
1605                    let src_value = serde_json::json!(src_cortical_idx);
1606                    if let Some(pos) = arr.iter().position(|v| v == &src_value) {
1607                        arr.remove(pos);
1608                        debug!(target: "feagi-bdu",
1609                            "Removed upstream area idx={} from cortical area '{}'",
1610                            src_cortical_idx, target_cortical_id.as_base_64()
1611                        );
1612                    }
1613                }
1614            }
1615        }
1616    }
1617
1618    /// Check if a cortical area exists
1619    pub fn has_cortical_area(&self, cortical_id: &CorticalID) -> bool {
1620        self.cortical_areas.contains_key(cortical_id)
1621    }
1622
1623    /// Check if the connectome is initialized (has areas)
1624    pub fn is_initialized(&self) -> bool {
1625        self.initialized && !self.cortical_areas.is_empty()
1626    }
1627
1628    // ======================================================================
1629    // Brain Region Management
1630    // ======================================================================
1631
1632    /// Add a brain region
1633    pub fn add_brain_region(
1634        &mut self,
1635        region: BrainRegion,
1636        parent_id: Option<String>,
1637    ) -> BduResult<()> {
1638        self.brain_regions.add_region(region, parent_id)?;
1639        self.refresh_brain_regions_hash();
1640        Ok(())
1641    }
1642
1643    /// Remove a brain region
1644    pub fn remove_brain_region(&mut self, region_id: &str) -> BduResult<()> {
1645        self.brain_regions.remove_region(region_id)?;
1646        self.refresh_brain_regions_hash();
1647        Ok(())
1648    }
1649
1650    /// Change the parent of an existing brain region.
1651    pub fn change_brain_region_parent(
1652        &mut self,
1653        region_id: &str,
1654        new_parent_id: &str,
1655    ) -> BduResult<()> {
1656        self.brain_regions.change_parent(region_id, new_parent_id)?;
1657        self.refresh_brain_regions_hash();
1658        Ok(())
1659    }
1660
1661    /// Get a brain region by ID
1662    pub fn get_brain_region(&self, region_id: &str) -> Option<&BrainRegion> {
1663        self.brain_regions.get_region(region_id)
1664    }
1665
1666    /// Get a mutable reference to a brain region
1667    pub fn get_brain_region_mut(&mut self, region_id: &str) -> Option<&mut BrainRegion> {
1668        self.brain_regions.get_region_mut(region_id)
1669    }
1670
1671    /// Get all brain region IDs
1672    pub fn get_brain_region_ids(&self) -> Vec<&String> {
1673        self.brain_regions.get_all_region_ids()
1674    }
1675
1676    /// Get the brain region hierarchy
1677    pub fn get_brain_region_hierarchy(&self) -> &BrainRegionHierarchy {
1678        &self.brain_regions
1679    }
1680
1681    // ========================================================================
1682    // MORPHOLOGY ACCESS
1683    // ========================================================================
1684
1685    /// Get all morphologies from the loaded genome
1686    pub fn get_morphologies(&self) -> &feagi_evolutionary::MorphologyRegistry {
1687        &self.morphology_registry
1688    }
1689
1690    /// Get morphology count
1691    pub fn get_morphology_count(&self) -> usize {
1692        self.morphology_registry.count()
1693    }
1694
1695    /// Insert or overwrite a morphology definition in the in-memory registry.
1696    ///
1697    /// NOTE: This updates the runtime registry used by mapping/synapse generation.
1698    /// Callers that also maintain a RuntimeGenome (source of truth) MUST update it too.
1699    pub fn upsert_morphology(
1700        &mut self,
1701        morphology_id: String,
1702        morphology: feagi_evolutionary::Morphology,
1703    ) {
1704        self.morphology_registry
1705            .add_morphology(morphology_id, morphology);
1706        self.refresh_morphologies_hash();
1707    }
1708
1709    /// Remove a morphology definition from the in-memory registry.
1710    ///
1711    /// Returns true if the morphology existed and was removed.
1712    ///
1713    /// NOTE: Callers that also maintain a RuntimeGenome (source of truth) MUST update it too.
1714    pub fn remove_morphology(&mut self, morphology_id: &str) -> bool {
1715        let removed = self.morphology_registry.remove_morphology(morphology_id);
1716        if removed {
1717            self.refresh_morphologies_hash();
1718        }
1719        removed
1720    }
1721
1722    // ========================================================================
1723    // CORTICAL MAPPING UPDATES
1724    // ========================================================================
1725
1726    /// Update cortical mapping properties between two cortical areas
1727    ///
1728    /// Following Python's update_cortical_mapping_properties() logic:
1729    /// 1. Updates the source area's cortical_mapping_dst property
1730    /// 2. Triggers synapse regeneration for the affected connection
1731    ///
1732    /// # Arguments
1733    /// * `src_area_id` - Source cortical area ID
1734    /// * `dst_area_id` - Destination cortical area ID
1735    /// * `mapping_data` - List of connection specifications
1736    ///
1737    /// # Returns
1738    /// * `BduResult<()>` - Ok if successful, Err otherwise
1739    pub fn update_cortical_mapping(
1740        &mut self,
1741        src_area_id: &CorticalID,
1742        dst_area_id: &CorticalID,
1743        mapping_data: Vec<serde_json::Value>,
1744    ) -> BduResult<()> {
1745        use tracing::info;
1746
1747        info!(target: "feagi-bdu", "Updating cortical mapping: {} -> {}", src_area_id, dst_area_id);
1748
1749        // Helper: detect associative_memory (bi-directional STDP) in a mapping payload
1750        let mapping_has_bidirectional_stdp = |rules: &[serde_json::Value]| -> bool {
1751            for rule in rules {
1752                if let Some(obj) = rule.as_object() {
1753                    if let Some(morphology_id) = obj.get("morphology_id").and_then(|v| v.as_str()) {
1754                        if morphology_id == "associative_memory" {
1755                            return true;
1756                        }
1757                    }
1758                }
1759            }
1760            false
1761        };
1762
1763        let requested_bidirectional = mapping_has_bidirectional_stdp(&mapping_data);
1764        let existing_bidirectional = self
1765            .cortical_areas
1766            .get(src_area_id)
1767            .and_then(|area| area.properties.get("cortical_mapping_dst"))
1768            .and_then(|v| v.as_object())
1769            .and_then(|map| map.get(&dst_area_id.as_base_64()))
1770            .and_then(|v| v.as_array())
1771            .map(|arr| mapping_has_bidirectional_stdp(arr))
1772            .unwrap_or(false);
1773        let should_apply_bidirectional = requested_bidirectional || existing_bidirectional;
1774
1775        {
1776            // Get source area (must exist)
1777            let src_area = self.cortical_areas.get_mut(src_area_id).ok_or_else(|| {
1778                crate::types::BduError::InvalidArea(format!(
1779                    "Source area not found: {}",
1780                    src_area_id
1781                ))
1782            })?;
1783
1784            // Get or create cortical_mapping_dst property
1785            let cortical_mapping_dst =
1786                if let Some(existing) = src_area.properties.get_mut("cortical_mapping_dst") {
1787                    existing.as_object_mut().ok_or_else(|| {
1788                        crate::types::BduError::InvalidMorphology(
1789                            "cortical_mapping_dst is not an object".to_string(),
1790                        )
1791                    })?
1792                } else {
1793                    // Create new cortical_mapping_dst
1794                    src_area
1795                        .properties
1796                        .insert("cortical_mapping_dst".to_string(), serde_json::json!({}));
1797                    src_area
1798                        .properties
1799                        .get_mut("cortical_mapping_dst")
1800                        .unwrap()
1801                        .as_object_mut()
1802                        .unwrap()
1803                };
1804
1805            // Update or add the mapping for this destination
1806            if mapping_data.is_empty() {
1807                // Empty mapping_data = delete the connection
1808                cortical_mapping_dst.remove(&dst_area_id.as_base_64());
1809                info!(target: "feagi-bdu", "Removed mapping from {} to {}", src_area_id, dst_area_id);
1810            } else {
1811                cortical_mapping_dst.insert(
1812                    dst_area_id.as_base_64(),
1813                    serde_json::Value::Array(mapping_data.clone()),
1814                );
1815                info!(target: "feagi-bdu", "Updated mapping from {} to {} with {} connections",
1816                      src_area_id, dst_area_id, mapping_data.len());
1817            }
1818        }
1819
1820        // Bi-directional STDP: mirror the mapping on the reverse pair
1821        if should_apply_bidirectional {
1822            let dst_area = self.cortical_areas.get_mut(dst_area_id).ok_or_else(|| {
1823                crate::types::BduError::InvalidArea(format!(
1824                    "Destination area not found: {}",
1825                    dst_area_id
1826                ))
1827            })?;
1828            let dst_mapping_dst =
1829                if let Some(existing) = dst_area.properties.get_mut("cortical_mapping_dst") {
1830                    existing.as_object_mut().ok_or_else(|| {
1831                        crate::types::BduError::InvalidMorphology(
1832                            "cortical_mapping_dst is not an object".to_string(),
1833                        )
1834                    })?
1835                } else {
1836                    dst_area
1837                        .properties
1838                        .insert("cortical_mapping_dst".to_string(), serde_json::json!({}));
1839                    dst_area
1840                        .properties
1841                        .get_mut("cortical_mapping_dst")
1842                        .unwrap()
1843                        .as_object_mut()
1844                        .unwrap()
1845                };
1846
1847            if mapping_data.is_empty() {
1848                dst_mapping_dst.remove(&src_area_id.as_base_64());
1849                info!(
1850                    target: "feagi-bdu",
1851                    "Removed bi-directional STDP mirror mapping from {} to {}",
1852                    dst_area_id,
1853                    src_area_id
1854                );
1855            } else {
1856                dst_mapping_dst.insert(
1857                    src_area_id.as_base_64(),
1858                    serde_json::Value::Array(mapping_data.clone()),
1859                );
1860                info!(
1861                    target: "feagi-bdu",
1862                    "Updated bi-directional STDP mirror mapping from {} to {} with {} connections",
1863                    dst_area_id,
1864                    src_area_id,
1865                    mapping_data.len()
1866                );
1867            }
1868        }
1869
1870        self.refresh_cortical_mappings_hash();
1871
1872        Ok(())
1873    }
1874
1875    /// Regenerate synapses for a specific cortical mapping
1876    ///
1877    /// Creates new synapses based on mapping rules. Only removes existing synapses if
1878    /// a mapping already existed (update case), not for new mappings (allows multiple
1879    /// synapses between the same neurons).
1880    ///
1881    /// # Arguments
1882    /// * `src_area_id` - Source cortical area ID
1883    /// * `dst_area_id` - Destination cortical area ID
1884    ///
1885    /// # Returns
1886    /// * `BduResult<usize>` - Number of synapses created
1887    pub fn regenerate_synapses_for_mapping(
1888        &mut self,
1889        src_area_id: &CorticalID,
1890        dst_area_id: &CorticalID,
1891    ) -> BduResult<usize> {
1892        use tracing::info;
1893
1894        info!(target: "feagi-bdu", "Regenerating synapses: {} -> {}", src_area_id, dst_area_id);
1895
1896        let mapping_rules_len = self
1897            .cortical_areas
1898            .get(src_area_id)
1899            .and_then(|area| area.properties.get("cortical_mapping_dst"))
1900            .and_then(|v| v.as_object())
1901            .and_then(|map| map.get(&dst_area_id.as_base_64()))
1902            .and_then(|v| v.as_array())
1903            .map(|arr| arr.len())
1904            .unwrap_or(0);
1905        tracing::debug!(
1906            target: "feagi-bdu",
1907            "Mapping rules for {} -> {}: {}",
1908            src_area_id,
1909            dst_area_id,
1910            mapping_rules_len
1911        );
1912
1913        // If NPU is available, regenerate synapses
1914        let Some(npu_arc) = self.npu.clone() else {
1915            info!(target: "feagi-bdu", "NPU not available - skipping synapse regeneration");
1916            return Ok(0);
1917        };
1918
1919        // Mapping regeneration must be deterministic:
1920        // - On mapping deletion: prune all synapses from A→B, then attempt synaptogenesis (which yields 0).
1921        // - On rule removal/updates: prune all synapses from A→B, then re-run synaptogenesis using the *current*
1922        //   mapping rules. This guarantees stale synapses from removed rules do not persist, while preserving
1923        //   other A→B mappings by re-creating them from the remaining rules.
1924        //
1925        // NOTE: This pruning requires retrieving neuron IDs in each area. Today, that can be O(all_neurons)
1926        // via `get_neurons_in_cortical_area()`. This is the safest correctness-first behavior.
1927
1928        let src_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
1929            BduError::InvalidArea(format!("No cortical idx for source area {}", src_area_id))
1930        })?;
1931        let dst_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
1932            BduError::InvalidArea(format!(
1933                "No cortical idx for destination area {}",
1934                dst_area_id
1935            ))
1936        })?;
1937
1938        // Prune all existing synapses from src_area→dst_area before (re)creating based on current rules.
1939        // This prevents stale synapses when rules are removed/edited.
1940        let mut pruned_synapse_count: usize = 0;
1941        use std::time::Instant;
1942        let start = Instant::now();
1943
1944        // Get neuron lists (may be slow; see note above).
1945        //
1946        // IMPORTANT: Do not rely on per-area cached neuron counts here. Pruning must be correct even if
1947        // caches are stale (e.g., in tests or during partial initialization). If either side is empty,
1948        // pruning is a no-op anyway.
1949        let (sources, targets) = {
1950            let lock_start = std::time::Instant::now();
1951            let npu = npu_arc.lock().unwrap();
1952            let lock_wait = lock_start.elapsed();
1953            tracing::debug!(
1954                target: "feagi-bdu",
1955                "[NPU-LOCK] prune list lock wait {:.2}ms for {} -> {}",
1956                lock_wait.as_secs_f64() * 1000.0,
1957                src_area_id,
1958                dst_area_id
1959            );
1960            let sources: Vec<NeuronId> = npu
1961                .get_neurons_in_cortical_area(src_idx)
1962                .into_iter()
1963                .map(NeuronId)
1964                .collect();
1965            let targets: Vec<NeuronId> = npu
1966                .get_neurons_in_cortical_area(dst_idx)
1967                .into_iter()
1968                .map(NeuronId)
1969                .collect();
1970            (sources, targets)
1971        };
1972
1973        tracing::debug!(
1974            target: "feagi-bdu",
1975            "Prune synapses: {} sources, {} targets",
1976            sources.len(),
1977            targets.len()
1978        );
1979
1980        if !sources.is_empty() && !targets.is_empty() {
1981            let remove_start = Instant::now();
1982            pruned_synapse_count = {
1983                let lock_start = std::time::Instant::now();
1984                let mut npu = npu_arc.lock().unwrap();
1985                let lock_wait = lock_start.elapsed();
1986                tracing::debug!(
1987                    target: "feagi-bdu",
1988                    "[NPU-LOCK] prune remove lock wait {:.2}ms for {} -> {}",
1989                    lock_wait.as_secs_f64() * 1000.0,
1990                    src_area_id,
1991                    dst_area_id
1992                );
1993                npu.remove_synapses_from_sources_to_targets(sources, targets)
1994            };
1995            let remove_time = remove_start.elapsed();
1996            let total_time = start.elapsed();
1997
1998            info!(
1999                target: "feagi-bdu",
2000                "Pruned {} existing synapses for mapping {} -> {} (total={}ms, remove={}ms)",
2001                pruned_synapse_count,
2002                src_area_id,
2003                dst_area_id,
2004                total_time.as_millis(),
2005                remove_time.as_millis()
2006            );
2007
2008            // Update StateManager synapse count (health_check endpoint)
2009            if pruned_synapse_count > 0 {
2010                let pruned_u32 = u32::try_from(pruned_synapse_count).map_err(|_| {
2011                    BduError::Internal(format!(
2012                        "Pruned synapse count overflow (usize -> u32): {}",
2013                        pruned_synapse_count
2014                    ))
2015                })?;
2016                if let Some(state_manager) = StateManager::instance().try_read() {
2017                    let core_state = state_manager.get_core_state();
2018                    core_state.subtract_synapse_count(pruned_u32);
2019                    state_manager.subtract_cortical_area_outgoing_synapses(
2020                        &src_area_id.as_base_64(),
2021                        pruned_synapse_count,
2022                    );
2023                    state_manager.subtract_cortical_area_incoming_synapses(
2024                        &dst_area_id.as_base_64(),
2025                        pruned_synapse_count,
2026                    );
2027                }
2028
2029                // Best-effort: adjust per-area outgoing synapse count cache for the source area.
2030                // (Cache is used for lock-free health-check reads; correctness is eventually
2031                // consistent via periodic refresh of global count from NPU.)
2032                {
2033                    let mut cache = self.cached_synapse_counts_per_area.write();
2034                    let entry = cache
2035                        .entry(*src_area_id)
2036                        .or_insert_with(|| AtomicUsize::new(0));
2037                    let mut current = entry.load(Ordering::Relaxed);
2038                    loop {
2039                        let next = current.saturating_sub(pruned_synapse_count);
2040                        match entry.compare_exchange(
2041                            current,
2042                            next,
2043                            Ordering::Relaxed,
2044                            Ordering::Relaxed,
2045                        ) {
2046                            Ok(_) => break,
2047                            Err(v) => current = v,
2048                        }
2049                    }
2050                }
2051            }
2052        }
2053
2054        // Apply cortical mapping rules to create synapses (may be 0 for memory areas).
2055        //
2056        // IMPORTANT:
2057        // - We already pruned A→B synapses above to ensure no stale synapses remain after a rule removal/update.
2058        // - `apply_cortical_mapping_for_pair()` returns the created synapse count but does not update
2059        //   StateManager/caches; we do that immediately after the call.
2060        let synapse_count = self.apply_cortical_mapping_for_pair(src_area_id, dst_area_id)?;
2061        tracing::debug!(
2062            target: "feagi-bdu",
2063            "Synaptogenesis created {} synapses for {} -> {}",
2064            synapse_count,
2065            src_area_id,
2066            dst_area_id
2067        );
2068
2069        // Update synapse count caches and StateManager based on synapses created.
2070        // NOTE: apply_cortical_mapping_for_pair() does not touch caches/StateManager.
2071        if synapse_count > 0 {
2072            let created_u32 = u32::try_from(synapse_count).map_err(|_| {
2073                BduError::Internal(format!(
2074                    "Created synapse count overflow (usize -> u32): {}",
2075                    synapse_count
2076                ))
2077            })?;
2078
2079            // Update per-area outgoing synapse count cache (source area)
2080            {
2081                let mut cache = self.cached_synapse_counts_per_area.write();
2082                cache
2083                    .entry(*src_area_id)
2084                    .or_insert_with(|| AtomicUsize::new(0))
2085                    .fetch_add(synapse_count, Ordering::Relaxed);
2086            }
2087
2088            // Update StateManager synapse count (health_check endpoint)
2089            if let Some(state_manager) = StateManager::instance().try_read() {
2090                let core_state = state_manager.get_core_state();
2091                core_state.add_synapse_count(created_u32);
2092                state_manager
2093                    .add_cortical_area_outgoing_synapses(&src_area_id.as_base_64(), synapse_count);
2094                state_manager
2095                    .add_cortical_area_incoming_synapses(&dst_area_id.as_base_64(), synapse_count);
2096            }
2097        }
2098
2099        // Update upstream area tracking based on MAPPING existence, not synapse count
2100        // Memory areas have 0 synapses but still need upstream tracking for pattern detection
2101        let src_idx_for_upstream = src_idx;
2102
2103        // Check if mapping exists by looking at cortical_mapping_dst property (after update)
2104        let has_mapping = self
2105            .cortical_areas
2106            .get(src_area_id)
2107            .and_then(|area| area.properties.get("cortical_mapping_dst"))
2108            .and_then(|v| v.as_object())
2109            .and_then(|map| map.get(&dst_area_id.as_base_64()))
2110            .is_some();
2111
2112        info!(target: "feagi-bdu",
2113            "Mapping result: {} synapses, {} -> {} (mapping_exists={}, will {}update upstream)",
2114            synapse_count,
2115            src_area_id.as_base_64(),
2116            dst_area_id.as_base_64(),
2117            has_mapping,
2118            if has_mapping { "" } else { "NOT " }
2119        );
2120
2121        if has_mapping {
2122            // Mapping exists - add to upstream tracking (for both memory and regular areas)
2123            self.add_upstream_area(dst_area_id, src_idx_for_upstream);
2124
2125            if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2126                if matches!(dst_area.cortical_type, CorticalAreaType::Memory(_)) {
2127                    if let Err(e) = self.ensure_memory_twin_area(dst_area_id, src_area_id) {
2128                        warn!(
2129                            target: "feagi-bdu",
2130                            "Failed to ensure memory twin for {} -> {}: {}",
2131                            src_area_id.as_base_64(),
2132                            dst_area_id.as_base_64(),
2133                            e
2134                        );
2135                    }
2136                }
2137            }
2138
2139            // If destination is a memory area, register it with PlasticityExecutor (automatic)
2140            #[cfg(feature = "plasticity")]
2141            if let Some(ref executor) = self.plasticity_executor {
2142                use feagi_evolutionary::extract_memory_properties;
2143
2144                if let Some(dst_area) = self.cortical_areas.get(dst_area_id) {
2145                    if let Some(mem_props) = extract_memory_properties(&dst_area.properties) {
2146                        let upstream_areas = self.get_upstream_cortical_areas(dst_area_id);
2147                        let upstream_non_memory =
2148                            self.filter_non_memory_upstream_areas(&upstream_areas);
2149                        debug!(
2150                            target: "feagi-bdu",
2151                            "Registering memory area idx={} id={} upstream={} depth={}",
2152                            dst_area.cortical_idx,
2153                            dst_area_id.as_base_64(),
2154                            upstream_areas.len(),
2155                            mem_props.temporal_depth
2156                        );
2157
2158                        // Ensure FireLedger tracks the upstream areas with at least the required temporal depth.
2159                        // Dense, burst-aligned tracking is required for correct memory pattern hashing.
2160                        if let Some(ref npu_arc) = self.npu {
2161                            if let Ok(mut npu) = npu_arc.lock() {
2162                                let existing_configs = npu.get_all_fire_ledger_configs();
2163                                for &upstream_idx in &upstream_areas {
2164                                    let existing = existing_configs
2165                                        .iter()
2166                                        .find(|(idx, _)| *idx == upstream_idx)
2167                                        .map(|(_, w)| *w)
2168                                        .unwrap_or(0);
2169
2170                                    let desired = mem_props.temporal_depth as usize;
2171                                    let resolved = existing.max(desired);
2172                                    if resolved != existing {
2173                                        if let Err(e) =
2174                                            npu.configure_fire_ledger_window(upstream_idx, resolved)
2175                                        {
2176                                            warn!(
2177                                                target: "feagi-bdu",
2178                                                "Failed to configure FireLedger window for upstream area idx={} (requested={}): {}",
2179                                                upstream_idx,
2180                                                resolved,
2181                                                e
2182                                            );
2183                                        }
2184                                    }
2185                                }
2186                            } else {
2187                                warn!(target: "feagi-bdu", "Failed to lock NPU for FireLedger configuration");
2188                            }
2189                        }
2190
2191                        if let Ok(exec) = executor.lock() {
2192                            use feagi_npu_plasticity::{
2193                                MemoryNeuronLifecycleConfig, PlasticityExecutor,
2194                            };
2195
2196                            let lifecycle_config = MemoryNeuronLifecycleConfig {
2197                                initial_lifespan: mem_props.init_lifespan,
2198                                lifespan_growth_rate: mem_props.lifespan_growth_rate,
2199                                longterm_threshold: mem_props.longterm_threshold,
2200                                max_reactivations: 1000,
2201                            };
2202
2203                            exec.register_memory_area(
2204                                dst_area.cortical_idx,
2205                                dst_area_id.as_base_64(),
2206                                mem_props.temporal_depth,
2207                                upstream_non_memory,
2208                                Some(lifecycle_config),
2209                            );
2210                        } else {
2211                            warn!(target: "feagi-bdu", "Failed to lock PlasticityExecutor");
2212                        }
2213                    } else {
2214                        debug!(
2215                            target: "feagi-bdu",
2216                            "Skipping plasticity registration: no memory properties for area {}",
2217                            dst_area_id.as_base_64()
2218                        );
2219                    }
2220                } else {
2221                    warn!(target: "feagi-bdu", "Destination area {} not found in cortical_areas", dst_area_id.as_base_64());
2222                }
2223            } else {
2224                warn!(
2225                    target: "feagi-bdu",
2226                    "PlasticityExecutor not available; memory area {} not registered",
2227                    dst_area_id.as_base_64()
2228                );
2229            }
2230
2231            #[cfg(not(feature = "plasticity"))]
2232            {
2233                info!(target: "feagi-bdu", "Plasticity feature disabled at compile time");
2234            }
2235        } else {
2236            // Mapping deleted - remove from upstream tracking
2237            self.remove_upstream_area(dst_area_id, src_idx_for_upstream);
2238
2239            // Ensure any STDP mapping parameters for this pair are removed when the mapping is gone.
2240            let mut npu = npu_arc.lock().unwrap();
2241            let _was_registered = npu.unregister_stdp_mapping(src_idx, dst_idx);
2242        }
2243
2244        info!(
2245            target: "feagi-bdu",
2246            "Created {} new synapses: {} -> {}",
2247            synapse_count,
2248            src_area_id,
2249            dst_area_id
2250        );
2251
2252        // CRITICAL: Rebuild synapse index so removals are reflected in propagation and query paths.
2253        // Many morphology paths rebuild the index after creation, but pruning requires an explicit rebuild.
2254        if pruned_synapse_count > 0 || synapse_count == 0 {
2255            let mut npu = npu_arc.lock().unwrap();
2256            npu.rebuild_synapse_index();
2257            info!(
2258                target: "feagi-bdu",
2259                "Rebuilt synapse index after regenerating {} -> {} (pruned={}, created={})",
2260                src_area_id,
2261                dst_area_id,
2262                pruned_synapse_count,
2263                synapse_count
2264            );
2265        } else {
2266            info!(
2267                target: "feagi-bdu",
2268                "Skipped synapse index rebuild for mapping {} -> {} (created={}, pruned=0; index rebuilt during synaptogenesis)",
2269                src_area_id,
2270                dst_area_id,
2271                synapse_count
2272            );
2273        }
2274
2275        // Refresh the global synapse count cache from NPU (deterministic after prune/create).
2276        {
2277            let npu = npu_arc.lock().unwrap();
2278            let fresh_count = npu.get_synapse_count();
2279            self.cached_synapse_count
2280                .store(fresh_count, Ordering::Relaxed);
2281        }
2282
2283        Ok(synapse_count)
2284    }
2285
2286    /// Register STDP mapping parameters for a plastic rule
2287    #[allow(clippy::too_many_arguments)]
2288    fn register_stdp_mapping_for_rule(
2289        npu: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
2290        src_area_id: &CorticalID,
2291        dst_area_id: &CorticalID,
2292        src_cortical_idx: u32,
2293        dst_cortical_idx: u32,
2294        rule_obj: &serde_json::Map<String, serde_json::Value>,
2295        bidirectional_stdp: bool,
2296        synapse_psp: u8,
2297        synapse_type: feagi_npu_neural::SynapseType,
2298    ) -> BduResult<()> {
2299        let plasticity_window = rule_obj
2300            .get("plasticity_window")
2301            .and_then(|v| v.as_u64())
2302            .ok_or_else(|| {
2303                BduError::Internal(format!(
2304                    "Missing plasticity_window in plastic mapping rule {} -> {}",
2305                    src_area_id, dst_area_id
2306                ))
2307            })? as usize;
2308        let plasticity_constant = rule_obj
2309            .get("plasticity_constant")
2310            .and_then(|v| v.as_i64())
2311            .ok_or_else(|| {
2312                BduError::Internal(format!(
2313                    "Missing plasticity_constant in plastic mapping rule {} -> {}",
2314                    src_area_id, dst_area_id
2315                ))
2316            })?;
2317        let ltp_multiplier = rule_obj
2318            .get("ltp_multiplier")
2319            .and_then(|v| v.as_i64())
2320            .ok_or_else(|| {
2321                BduError::Internal(format!(
2322                    "Missing ltp_multiplier in plastic mapping rule {} -> {}",
2323                    src_area_id, dst_area_id
2324                ))
2325            })?;
2326        let ltd_multiplier = rule_obj
2327            .get("ltd_multiplier")
2328            .and_then(|v| v.as_i64())
2329            .ok_or_else(|| {
2330                BduError::Internal(format!(
2331                    "Missing ltd_multiplier in plastic mapping rule {} -> {}",
2332                    src_area_id, dst_area_id
2333                ))
2334            })?;
2335
2336        let params = feagi_npu_burst_engine::npu::StdpMappingParams {
2337            plasticity_window,
2338            plasticity_constant,
2339            ltp_multiplier,
2340            ltd_multiplier,
2341            bidirectional_stdp,
2342            synapse_psp,
2343            synapse_type,
2344        };
2345
2346        trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: attempting NPU lock");
2347        let mut npu_lock = npu
2348            .lock()
2349            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
2350        trace!(target: "feagi-bdu", "[LOCK-TRACE] create_neurons_for_area: acquired NPU lock");
2351
2352        npu_lock
2353            .register_stdp_mapping(src_cortical_idx, dst_cortical_idx, params)
2354            .map_err(|e| {
2355                BduError::Internal(format!(
2356                    "Failed to register STDP mapping {} -> {}: {}",
2357                    src_area_id, dst_area_id, e
2358                ))
2359            })?;
2360
2361        // FireLedger tracking for STDP (ensure A and B are tracked at least to plasticity_window)
2362        let existing_configs = npu_lock.get_all_fire_ledger_configs();
2363        for area_idx in [src_cortical_idx, dst_cortical_idx] {
2364            let existing = existing_configs
2365                .iter()
2366                .find(|(idx, _)| *idx == area_idx)
2367                .map(|(_, w)| *w)
2368                .unwrap_or(0);
2369            let resolved = existing.max(plasticity_window);
2370            if resolved != existing {
2371                npu_lock
2372                    .configure_fire_ledger_window(area_idx, resolved)
2373                    .map_err(|e| {
2374                        BduError::Internal(format!(
2375                            "Failed to configure FireLedger window for area idx={} (requested={}): {}",
2376                            area_idx, resolved, e
2377                        ))
2378                    })?;
2379            }
2380        }
2381
2382        Ok(())
2383    }
2384
2385    /// Resolve synapse weight, PSP, and type from a mapping rule.
2386    fn resolve_synapse_params_for_rule(
2387        &self,
2388        src_area_id: &CorticalID,
2389        rule: &serde_json::Value,
2390    ) -> BduResult<(u8, u8, feagi_npu_neural::SynapseType)> {
2391        // Get source area to access PSP property
2392        let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2393            crate::types::BduError::InvalidArea(format!("Source area not found: {}", src_area_id))
2394        })?;
2395
2396        // Extract weight from rule (postSynapticCurrent_multiplier)
2397        //
2398        // IMPORTANT:
2399        // - This value represents the synapse "weight" stored in the NPU (u8: 0..255).
2400        // - Do NOT scale by 255 here. A multiplier of 1.0 should remain weight=1 (not 255).
2401        let (weight, synapse_type) = {
2402            // Accept either integer or whole-number float inputs for compatibility with older clients/tests.
2403            let parse_i64 = |v: &serde_json::Value| -> Option<i64> {
2404                if let Some(i) = v.as_i64() {
2405                    return Some(i);
2406                }
2407                let f = v.as_f64()?;
2408                if f.fract() == 0.0 {
2409                    Some(f as i64)
2410                } else {
2411                    None
2412                }
2413            };
2414
2415            let multiplier_i64: i64 = if let Some(obj) = rule.as_object() {
2416                obj.get("postSynapticCurrent_multiplier")
2417                    .and_then(parse_i64)
2418                    .unwrap_or(1) // @architecture:acceptable - rule-level default multiplier
2419            } else if let Some(arr) = rule.as_array() {
2420                // Array format: [morphology_id, scalar, multiplier, ...]
2421                arr.get(2).and_then(parse_i64).unwrap_or(1) // @architecture:acceptable - rule-level default multiplier
2422            } else {
2423                128 // @architecture:acceptable - emergency fallback for malformed rule
2424            };
2425
2426            if multiplier_i64 < 0 {
2427                let abs = if multiplier_i64 == i64::MIN {
2428                    i64::MAX
2429                } else {
2430                    multiplier_i64.abs()
2431                };
2432                (
2433                    abs.clamp(0, 255) as u8,
2434                    feagi_npu_neural::SynapseType::Inhibitory,
2435                )
2436            } else {
2437                (
2438                    multiplier_i64.clamp(0, 255) as u8,
2439                    feagi_npu_neural::SynapseType::Excitatory,
2440                )
2441            }
2442        };
2443
2444        // Get PSP from source cortical area.
2445        //
2446        // IMPORTANT:
2447        // - This value represents the synapse PSP stored in the NPU (u8: 0..255).
2448        // - Treat `postsynaptic_current` as an absolute value in 0..255 units.
2449        // - Do NOT scale by 255 here. A PSP of 1.0 should remain PSP=1 (not 255).
2450        let (psp_f32, psp) = {
2451            use crate::models::cortical_area::CorticalAreaExt;
2452            let psp_f32 = src_area.postsynaptic_current();
2453            (psp_f32, psp_f32.clamp(0.0, 255.0) as u8)
2454        };
2455
2456        tracing::debug!(
2457            target: "feagi-bdu",
2458            "Resolved synapse params src={} weight={} psp={} psp_f32={} type={:?}",
2459            src_area_id.as_base_64(),
2460            weight,
2461            psp,
2462            psp_f32,
2463            synapse_type
2464        );
2465
2466        Ok((weight, psp, synapse_type))
2467    }
2468
2469    /// Apply cortical mapping for a specific area pair
2470    fn apply_cortical_mapping_for_pair(
2471        &mut self,
2472        src_area_id: &CorticalID,
2473        dst_area_id: &CorticalID,
2474    ) -> BduResult<usize> {
2475        // Clone the rules to avoid borrow checker issues.
2476        //
2477        // IMPORTANT: absence of mapping rules is a valid state (e.g. mapping deletion).
2478        // In that case, return Ok(0) rather than an error so API callers can treat
2479        // "deleted mapping" as success (and BV can update its cache/UI).
2480        let rules = {
2481            let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2482                crate::types::BduError::InvalidArea(format!(
2483                    "Source area not found: {}",
2484                    src_area_id
2485                ))
2486            })?;
2487
2488            let Some(mapping_dst) = src_area
2489                .properties
2490                .get("cortical_mapping_dst")
2491                .and_then(|v| v.as_object())
2492            else {
2493                return Ok(0);
2494            };
2495
2496            let Some(rules) = mapping_dst
2497                .get(&dst_area_id.as_base_64())
2498                .and_then(|v| v.as_array())
2499            else {
2500                return Ok(0);
2501            };
2502
2503            rules.clone()
2504        }; // Borrow ends here
2505
2506        if rules.is_empty() {
2507            return Ok(0);
2508        }
2509
2510        // Get indices for STDP handling
2511        let src_cortical_idx = *self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
2512            crate::types::BduError::InvalidArea(format!("No index for {}", src_area_id))
2513        })?;
2514        let dst_cortical_idx = *self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
2515            crate::types::BduError::InvalidArea(format!("No index for {}", dst_area_id))
2516        })?;
2517
2518        // Clone NPU Arc for STDP handling (Arc::clone is cheap - just increments ref count)
2519        let npu_arc = self
2520            .npu
2521            .as_ref()
2522            .ok_or_else(|| crate::types::BduError::Internal("NPU not connected".to_string()))?
2523            .clone();
2524
2525        tracing::debug!(
2526            target: "feagi-bdu",
2527            "Applying {} mapping rule(s) for {} -> {}",
2528            rules.len(),
2529            src_area_id,
2530            dst_area_id
2531        );
2532        // Apply each morphology rule
2533        let mut total_synapses = 0;
2534        for rule in &rules {
2535            let rule_obj = match rule.as_object() {
2536                Some(obj) => obj,
2537                None => continue,
2538            };
2539            let morphology_id = rule_obj
2540                .get("morphology_id")
2541                .and_then(|v| v.as_str())
2542                .unwrap_or("unknown");
2543
2544            let rule_keys: Vec<String> = rule_obj.keys().cloned().collect();
2545
2546            // Handle STDP/plasticity configuration if needed
2547            let mut plasticity_flag = rule_obj
2548                .get("plasticity_flag")
2549                .and_then(|v| v.as_bool())
2550                .unwrap_or(false);
2551            if morphology_id == "associative_memory" {
2552                plasticity_flag = true;
2553            }
2554            if plasticity_flag {
2555                let (_weight, psp, synapse_type) =
2556                    self.resolve_synapse_params_for_rule(src_area_id, rule)?;
2557                let bidirectional_stdp = morphology_id == "associative_memory";
2558                if let Err(e) = Self::register_stdp_mapping_for_rule(
2559                    &npu_arc,
2560                    src_area_id,
2561                    dst_area_id,
2562                    src_cortical_idx,
2563                    dst_cortical_idx,
2564                    rule_obj,
2565                    bidirectional_stdp,
2566                    psp,
2567                    synapse_type,
2568                ) {
2569                    tracing::error!(
2570                        target: "feagi-bdu",
2571                        "STDP mapping registration failed for {} -> {} (morphology={}, keys={:?}): {}",
2572                        src_area_id,
2573                        dst_area_id,
2574                        morphology_id,
2575                        rule_keys,
2576                        e
2577                    );
2578                    return Err(e);
2579                }
2580            }
2581
2582            // Apply the morphology rule
2583            let synapse_count = match self.apply_single_morphology_rule(
2584                src_area_id,
2585                dst_area_id,
2586                rule,
2587            ) {
2588                Ok(count) => count,
2589                Err(e) => {
2590                    tracing::error!(
2591                        target: "feagi-bdu",
2592                        "Mapping rule application failed for {} -> {} (morphology={}, keys={:?}): {}",
2593                        src_area_id,
2594                        dst_area_id,
2595                        morphology_id,
2596                        rule_keys,
2597                        e
2598                    );
2599                    return Err(e);
2600                }
2601            };
2602            total_synapses += synapse_count;
2603            tracing::debug!(
2604                target: "feagi-bdu",
2605                "Rule {} created {} synapses for {} -> {}",
2606                morphology_id,
2607                synapse_count,
2608                src_area_id,
2609                dst_area_id
2610            );
2611        }
2612
2613        Ok(total_synapses)
2614    }
2615
2616    /// Apply a function-type morphology (projector, memory, block_to_block, etc.)
2617    ///
2618    /// This helper consolidates all function-type morphology logic in one place.
2619    /// Function-type morphologies are code-driven and require code changes to add new ones.
2620    ///
2621    /// # Arguments
2622    /// * `morphology_id` - The morphology ID string (e.g., "projector", "block_to_block")
2623    /// * `rule` - The morphology rule JSON value
2624    /// * `npu_arc` - Arc to the NPU (for batched operations)
2625    /// * `npu` - Locked NPU reference
2626    /// * `src_area_id`, `dst_area_id` - Source and destination area IDs
2627    /// * `src_idx`, `dst_idx` - Source and destination area indices
2628    /// * `weight`, `psp`, `synapse_attractivity` - Synapse parameters
2629    #[allow(clippy::too_many_arguments)]
2630    fn apply_function_morphology(
2631        &self,
2632        morphology_id: &str,
2633        rule: &serde_json::Value,
2634        npu_arc: &Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
2635        npu: &mut feagi_npu_burst_engine::DynamicNPU,
2636        src_area_id: &CorticalID,
2637        dst_area_id: &CorticalID,
2638        src_idx: u32,
2639        dst_idx: u32,
2640        weight: u8,
2641        psp: u8,
2642        synapse_attractivity: u8,
2643        synapse_type: feagi_npu_neural::SynapseType,
2644    ) -> BduResult<usize> {
2645        match morphology_id {
2646            "projector" => {
2647                // Get dimensions from cortical areas (no neuron scanning!)
2648                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2649                    crate::types::BduError::InvalidArea(format!(
2650                        "Source area not found: {}",
2651                        src_area_id
2652                    ))
2653                })?;
2654                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2655                    crate::types::BduError::InvalidArea(format!(
2656                        "Destination area not found: {}",
2657                        dst_area_id
2658                    ))
2659                })?;
2660
2661                let src_dimensions = (
2662                    src_area.dimensions.width as usize,
2663                    src_area.dimensions.height as usize,
2664                    src_area.dimensions.depth as usize,
2665                );
2666                let dst_dimensions = (
2667                    dst_area.dimensions.width as usize,
2668                    dst_area.dimensions.height as usize,
2669                    dst_area.dimensions.depth as usize,
2670                );
2671
2672                use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
2673                let count = apply_projector_morphology_with_dimensions(
2674                    npu,
2675                    src_idx,
2676                    dst_idx,
2677                    src_dimensions,
2678                    dst_dimensions,
2679                    None, // transpose
2680                    None, // project_last_layer_of
2681                    weight,
2682                    psp,
2683                    synapse_attractivity,
2684                    synapse_type,
2685                )?;
2686                // Ensure the propagation engine sees the newly created synapses immediately
2687                npu.rebuild_synapse_index();
2688                Ok(count as usize)
2689            }
2690            "episodic_memory" => {
2691                // Episodic memory morphology: No physical synapses created
2692                // Pattern detection and memory neuron creation handled by PlasticityService
2693                use tracing::trace;
2694                trace!(
2695                    target: "feagi-bdu",
2696                    "Episodic memory morphology: {} -> {} (no physical synapses, plasticity-driven)",
2697                    src_idx, dst_idx
2698                );
2699                Ok(0)
2700            }
2701            "memory_replay" => {
2702                // Replay mapping: semantic only, no physical synapses
2703                use tracing::trace;
2704                trace!(
2705                    target: "feagi-bdu",
2706                    "Memory replay morphology: {} -> {} (no physical synapses)",
2707                    src_idx, dst_idx
2708                );
2709                Ok(0)
2710            }
2711            "associative_memory" => {
2712                // Associative memory (bi-directional STDP) mapping:
2713                // If both ends are memory areas, create synapses between LIF twins to enable associations.
2714                // Otherwise, no initial synapses are created (STDP will update existing synapses).
2715                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2716                    crate::types::BduError::InvalidArea(format!(
2717                        "Source area not found: {}",
2718                        src_area_id
2719                    ))
2720                })?;
2721                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2722                    crate::types::BduError::InvalidArea(format!(
2723                        "Destination area not found: {}",
2724                        dst_area_id
2725                    ))
2726                })?;
2727
2728                if matches!(src_area.cortical_type, CorticalAreaType::Memory(_))
2729                    && matches!(dst_area.cortical_type, CorticalAreaType::Memory(_))
2730                {
2731                    let src_dimensions = (
2732                        src_area.dimensions.width as usize,
2733                        src_area.dimensions.height as usize,
2734                        src_area.dimensions.depth as usize,
2735                    );
2736                    let dst_dimensions = (
2737                        dst_area.dimensions.width as usize,
2738                        dst_area.dimensions.height as usize,
2739                        dst_area.dimensions.depth as usize,
2740                    );
2741                    use crate::connectivity::core_morphologies::apply_projector_morphology_with_dimensions;
2742                    let count = apply_projector_morphology_with_dimensions(
2743                        npu,
2744                        src_idx,
2745                        dst_idx,
2746                        src_dimensions,
2747                        dst_dimensions,
2748                        None,
2749                        None,
2750                        weight,
2751                        psp,
2752                        synapse_attractivity,
2753                        synapse_type,
2754                    )?;
2755                    npu.rebuild_synapse_index();
2756                    Ok(count as usize)
2757                } else {
2758                    Ok(0)
2759                }
2760            }
2761            "block_to_block" => {
2762                tracing::warn!(
2763                    target: "feagi-bdu",
2764                    "🔍 DEBUG apply_function_morphology: block_to_block case reached with src_idx={}, dst_idx={}",
2765                    src_idx, dst_idx
2766                );
2767                // Get dimensions from cortical areas (no neuron scanning!)
2768                let src_area = self.cortical_areas.get(src_area_id).ok_or_else(|| {
2769                    crate::types::BduError::InvalidArea(format!(
2770                        "Source area not found: {}",
2771                        src_area_id
2772                    ))
2773                })?;
2774                let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2775                    crate::types::BduError::InvalidArea(format!(
2776                        "Destination area not found: {}",
2777                        dst_area_id
2778                    ))
2779                })?;
2780
2781                let src_dimensions = (
2782                    src_area.dimensions.width as usize,
2783                    src_area.dimensions.height as usize,
2784                    src_area.dimensions.depth as usize,
2785                );
2786                let dst_dimensions = (
2787                    dst_area.dimensions.width as usize,
2788                    dst_area.dimensions.height as usize,
2789                    dst_area.dimensions.depth as usize,
2790                );
2791
2792                // Extract scalar from rule (morphology_scalar)
2793                let scalar = if let Some(obj) = rule.as_object() {
2794                    // Object format: get from morphology_scalar array
2795                    if let Some(scalar_arr) =
2796                        obj.get("morphology_scalar").and_then(|v| v.as_array())
2797                    {
2798                        // Use first element as scalar (or default to 1)
2799                        scalar_arr.first().and_then(|v| v.as_i64()).unwrap_or(1) as u32
2800                    } else {
2801                        1 // @architecture:acceptable - default scalar
2802                    }
2803                } else if let Some(arr) = rule.as_array() {
2804                    // Array format: [morphology_id, scalar, multiplier, ...]
2805                    arr.get(1).and_then(|v| v.as_i64()).unwrap_or(1) as u32
2806                } else {
2807                    1 // @architecture:acceptable - default scalar
2808                };
2809
2810                // CRITICAL: Do NOT call get_neurons_in_cortical_area to check neuron count!
2811                // Use dimensions to estimate: if area is large, use batched version
2812                let estimated_neurons = src_dimensions.0 * src_dimensions.1 * src_dimensions.2;
2813                let count = if estimated_neurons > 100_000 {
2814                    // Release lock and use batched version
2815                    let _ = npu;
2816
2817                    crate::connectivity::synaptogenesis::apply_block_connection_morphology_batched(
2818                        npu_arc,
2819                        src_idx,
2820                        dst_idx,
2821                        src_dimensions,
2822                        dst_dimensions,
2823                        scalar, // scaling_factor
2824                        weight,
2825                        psp,
2826                        synapse_attractivity,
2827                        synapse_type,
2828                    )? as usize
2829                } else {
2830                    // Small area: use regular version (faster for small counts)
2831                    tracing::warn!(
2832                        target: "feagi-bdu",
2833                        "🔍 DEBUG connectome_manager: Calling apply_block_connection_morphology with src_idx={}, dst_idx={}, src_dim={:?}, dst_dim={:?}",
2834                        src_idx, dst_idx, src_dimensions, dst_dimensions
2835                    );
2836                    let count =
2837                        crate::connectivity::synaptogenesis::apply_block_connection_morphology(
2838                            npu,
2839                            src_idx,
2840                            dst_idx,
2841                            src_dimensions,
2842                            dst_dimensions,
2843                            scalar, // scaling_factor
2844                            weight,
2845                            psp,
2846                            synapse_attractivity,
2847                            synapse_type,
2848                        )? as usize;
2849                    tracing::warn!(
2850                        target: "feagi-bdu",
2851                        "🔍 DEBUG connectome_manager: apply_block_connection_morphology returned count={}",
2852                        count
2853                    );
2854                    // Rebuild synapse index while we still have the lock
2855                    if count > 0 {
2856                        npu.rebuild_synapse_index();
2857                    }
2858                    count
2859                };
2860
2861                // Ensure the propagation engine sees the newly created synapses immediately (batched version only)
2862                if count > 0 && estimated_neurons > 100_000 {
2863                    let mut npu_lock = npu_arc.lock().unwrap();
2864                    npu_lock.rebuild_synapse_index();
2865                }
2866
2867                Ok(count)
2868            }
2869            _ => {
2870                // Other function morphologies not yet implemented
2871                // NOTE: To add a new function-type morphology, add a case here
2872                use tracing::debug;
2873                debug!(target: "feagi-bdu", "Function morphology {} not yet implemented", morphology_id);
2874                Ok(0)
2875            }
2876        }
2877    }
2878
2879    /// Apply a single morphology rule
2880    fn apply_single_morphology_rule(
2881        &mut self,
2882        src_area_id: &CorticalID,
2883        dst_area_id: &CorticalID,
2884        rule: &serde_json::Value,
2885    ) -> BduResult<usize> {
2886        // Extract morphology_id from rule (array or dict format)
2887        let morphology_id = if let Some(arr) = rule.as_array() {
2888            arr.first().and_then(|v| v.as_str()).unwrap_or("")
2889        } else if let Some(obj) = rule.as_object() {
2890            obj.get("morphology_id")
2891                .and_then(|v| v.as_str())
2892                .unwrap_or("")
2893        } else {
2894            return Ok(0);
2895        };
2896
2897        if morphology_id.is_empty() {
2898            return Ok(0);
2899        }
2900
2901        // Get morphology from registry
2902        let morphology = self.morphology_registry.get(morphology_id).ok_or_else(|| {
2903            crate::types::BduError::InvalidMorphology(format!(
2904                "Morphology not found: {}",
2905                morphology_id
2906            ))
2907        })?;
2908
2909        // Convert area IDs to cortical indices (required by NPU functions)
2910        let src_idx = self.cortical_id_to_idx.get(src_area_id).ok_or_else(|| {
2911            crate::types::BduError::InvalidArea(format!(
2912                "Source area ID not found: {}",
2913                src_area_id
2914            ))
2915        })?;
2916        let dst_idx = self.cortical_id_to_idx.get(dst_area_id).ok_or_else(|| {
2917            crate::types::BduError::InvalidArea(format!(
2918                "Destination area ID not found: {}",
2919                dst_area_id
2920            ))
2921        })?;
2922
2923        // Apply morphology based on type
2924        if let Some(ref npu_arc) = self.npu {
2925            let lock_start = std::time::Instant::now();
2926            let mut npu = npu_arc.lock().unwrap();
2927            let lock_wait = lock_start.elapsed();
2928            tracing::debug!(
2929                target: "feagi-bdu",
2930                "[NPU-LOCK] synaptogenesis lock wait {:.2}ms for {} -> {} (morphology={})",
2931                lock_wait.as_secs_f64() * 1000.0,
2932                src_area_id,
2933                dst_area_id,
2934                morphology_id
2935            );
2936
2937            let (weight, psp, synapse_type) =
2938                self.resolve_synapse_params_for_rule(src_area_id, rule)?;
2939
2940            // Extract synapse_attractivity from rule (probability 0-100)
2941            let synapse_attractivity = if let Some(obj) = rule.as_object() {
2942                obj.get("synapse_attractivity")
2943                    .and_then(|v| v.as_u64())
2944                    .unwrap_or(100) as u8
2945            } else {
2946                100 // @architecture:acceptable - default to always create when not specified
2947            };
2948
2949            match morphology.morphology_type {
2950                feagi_evolutionary::MorphologyType::Functions => {
2951                    tracing::warn!(
2952                        target: "feagi-bdu",
2953                        "🔍 DEBUG apply_single_morphology_rule: Functions type, morphology_id={}, calling apply_function_morphology",
2954                        morphology_id
2955                    );
2956                    // Function-based morphologies (projector, memory, block_to_block, etc.)
2957                    // Delegate to helper function to consolidate all function-type logic
2958                    self.apply_function_morphology(
2959                        morphology_id,
2960                        rule,
2961                        npu_arc,
2962                        &mut npu,
2963                        src_area_id,
2964                        dst_area_id,
2965                        *src_idx,
2966                        *dst_idx,
2967                        weight,
2968                        psp,
2969                        synapse_attractivity,
2970                        synapse_type,
2971                    )
2972                }
2973                feagi_evolutionary::MorphologyType::Vectors => {
2974                    use crate::connectivity::synaptogenesis::apply_vectors_morphology_with_dimensions;
2975
2976                    // Get dimensions from cortical areas (no neuron scanning!)
2977                    let dst_area = self.cortical_areas.get(dst_area_id).ok_or_else(|| {
2978                        crate::types::BduError::InvalidArea(format!(
2979                            "Destination area not found: {}",
2980                            dst_area_id
2981                        ))
2982                    })?;
2983
2984                    let dst_dimensions = (
2985                        dst_area.dimensions.width as usize,
2986                        dst_area.dimensions.height as usize,
2987                        dst_area.dimensions.depth as usize,
2988                    );
2989
2990                    if let feagi_evolutionary::MorphologyParameters::Vectors { ref vectors } =
2991                        morphology.parameters
2992                    {
2993                        // Convert Vec<[i32; 3]> to Vec<(i32, i32, i32)>
2994                        let vectors_tuples: Vec<(i32, i32, i32)> =
2995                            vectors.iter().map(|v| (v[0], v[1], v[2])).collect();
2996
2997                        let count = apply_vectors_morphology_with_dimensions(
2998                            &mut npu,
2999                            *src_idx,
3000                            *dst_idx,
3001                            vectors_tuples,
3002                            dst_dimensions,
3003                            weight,               // From rule, not hardcoded
3004                            psp,                  // PSP from source area, NOT hardcoded!
3005                            synapse_attractivity, // From rule, not hardcoded
3006                            synapse_type,
3007                        )?;
3008                        // Ensure the propagation engine sees the newly created synapses immediately,
3009                        // and avoid a second outer NPU mutex acquisition later in the mapping update path.
3010                        npu.rebuild_synapse_index();
3011                        Ok(count as usize)
3012                    } else {
3013                        Ok(0)
3014                    }
3015                }
3016                feagi_evolutionary::MorphologyType::Patterns => {
3017                    use crate::connectivity::core_morphologies::apply_patterns_morphology;
3018                    use crate::connectivity::rules::patterns::{
3019                        Pattern3D, PatternElement as RulePatternElement,
3020                    };
3021                    use feagi_evolutionary::PatternElement as EvoPatternElement;
3022
3023                    let feagi_evolutionary::MorphologyParameters::Patterns { ref patterns } =
3024                        morphology.parameters
3025                    else {
3026                        return Ok(0);
3027                    };
3028
3029                    let convert_element =
3030                        |element: &EvoPatternElement|
3031                         -> crate::types::BduResult<RulePatternElement> {
3032                            match element {
3033                                EvoPatternElement::Value(value) => {
3034                                    if *value < 0 {
3035                                        return Err(crate::types::BduError::InvalidMorphology(
3036                                            format!(
3037                                                "Pattern morphology {} contains negative voxel coordinate {}",
3038                                                morphology_id, value
3039                                            ),
3040                                        ));
3041                                    }
3042                                    Ok(RulePatternElement::Exact(*value))
3043                                }
3044                                EvoPatternElement::Wildcard => Ok(RulePatternElement::Wildcard),
3045                                EvoPatternElement::Skip => Ok(RulePatternElement::Skip),
3046                                EvoPatternElement::Exclude => Ok(RulePatternElement::Exclude),
3047                            }
3048                        };
3049
3050                    let mut converted_patterns = Vec::with_capacity(patterns.len());
3051                    for pattern_pair in patterns {
3052                        if pattern_pair.len() != 2 {
3053                            return Err(crate::types::BduError::InvalidMorphology(format!(
3054                                "Pattern morphology {} must contain [src, dst] pairs",
3055                                morphology_id
3056                            )));
3057                        }
3058
3059                        let src_pattern = &pattern_pair[0];
3060                        let dst_pattern = &pattern_pair[1];
3061
3062                        if src_pattern.len() != 3 || dst_pattern.len() != 3 {
3063                            return Err(crate::types::BduError::InvalidMorphology(format!(
3064                                "Pattern morphology {} requires 3-axis patterns",
3065                                morphology_id
3066                            )));
3067                        }
3068
3069                        let src: Pattern3D = (
3070                            convert_element(&src_pattern[0])?,
3071                            convert_element(&src_pattern[1])?,
3072                            convert_element(&src_pattern[2])?,
3073                        );
3074                        let dst: Pattern3D = (
3075                            convert_element(&dst_pattern[0])?,
3076                            convert_element(&dst_pattern[1])?,
3077                            convert_element(&dst_pattern[2])?,
3078                        );
3079
3080                        converted_patterns.push((src, dst));
3081                    }
3082
3083                    let count = apply_patterns_morphology(
3084                        &mut npu,
3085                        *src_idx,
3086                        *dst_idx,
3087                        converted_patterns,
3088                        weight,
3089                        psp,
3090                        synapse_attractivity,
3091                        synapse_type,
3092                    )?;
3093                    if count > 0 {
3094                        npu.rebuild_synapse_index();
3095                    }
3096                    Ok(count as usize)
3097                }
3098                _ => {
3099                    use tracing::debug;
3100                    debug!(target: "feagi-bdu", "Morphology type {:?} not yet fully implemented", morphology.morphology_type);
3101                    Ok(0)
3102                }
3103            }
3104        } else {
3105            Ok(0) // NPU not available
3106        }
3107    }
3108
3109    // ======================================================================
3110    // NPU Integration
3111    // ======================================================================
3112
3113    /// Set the NPU reference for neuron/synapse queries
3114    ///
3115    /// This should be called once during FEAGI initialization after the NPU is created.
3116    ///
3117    /// # Arguments
3118    ///
3119    /// * `npu` - Arc to the Rust NPU (wrapped in TracingMutex for automatic lock tracing)
3120    ///
3121    pub fn set_npu(
3122        &mut self,
3123        npu: Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>,
3124    ) {
3125        self.npu = Some(Arc::clone(&npu));
3126        info!(target: "feagi-bdu","🔗 ConnectomeManager: NPU reference set");
3127
3128        // CRITICAL: Update State Manager with capacity values (from config, never changes)
3129        // This ensures health check endpoint can read capacity without acquiring NPU lock
3130        #[cfg(not(feature = "wasm"))]
3131        {
3132            use feagi_state_manager::StateManager;
3133            if let Some(state_manager) = StateManager::instance().try_read() {
3134                let core_state = state_manager.get_core_state();
3135                // Capacity comes from config (set at initialization, never changes)
3136                core_state.set_neuron_capacity(self.config.max_neurons as u32);
3137                core_state.set_synapse_capacity(self.config.max_synapses as u32);
3138                info!(
3139                    target: "feagi-bdu",
3140                    "📊 Updated State Manager with capacity: {} neurons, {} synapses",
3141                    self.config.max_neurons, self.config.max_synapses
3142                );
3143            }
3144        }
3145
3146        // CRITICAL: Backfill cortical area registrations into NPU.
3147        //
3148        // Cortical areas can be created/loaded before the NPU is attached (startup ordering).
3149        // Those areas won't be registered via `add_cortical_area()` (it registers only if NPU is present),
3150        // which causes visualization encoding to fall back to "area_{idx}" and subsequently drop the area
3151        // (base64 decode fails), making BV appear to "miss" firing activity for that cortical area.
3152        let existing_area_count = self.cortical_id_to_idx.len();
3153        if existing_area_count > 0 {
3154            match npu.lock() {
3155                Ok(mut npu_lock) => {
3156                    for (cortical_id, cortical_idx) in self.cortical_id_to_idx.iter() {
3157                        npu_lock.register_cortical_area(*cortical_idx, cortical_id.as_base_64());
3158                    }
3159                    info!(
3160                        target: "feagi-bdu",
3161                        "🔁 Backfilled {} cortical area registrations into NPU",
3162                        existing_area_count
3163                    );
3164                }
3165                Err(e) => {
3166                    warn!(
3167                        target: "feagi-bdu",
3168                        "⚠️ Failed to lock NPU for cortical area backfill registration: {}",
3169                        e
3170                    );
3171                }
3172            }
3173        }
3174
3175        // Initialize cached stats immediately
3176        self.update_all_cached_stats();
3177        info!(target: "feagi-bdu","📊 Initialized cached stats: {} neurons, {} synapses",
3178            self.get_neuron_count(), self.get_synapse_count());
3179    }
3180
3181    /// Check if NPU is connected
3182    pub fn has_npu(&self) -> bool {
3183        self.npu.is_some()
3184    }
3185
3186    /// Get NPU reference (read-only access for queries)
3187    ///
3188    /// # Returns
3189    ///
3190    /// * `Option<&Arc<Mutex<RustNPU>>>` - Reference to NPU if connected
3191    ///
3192    pub fn get_npu(
3193        &self,
3194    ) -> Option<&Arc<feagi_npu_burst_engine::TracingMutex<feagi_npu_burst_engine::DynamicNPU>>>
3195    {
3196        self.npu.as_ref()
3197    }
3198
3199    /// Set the PlasticityExecutor reference (optional, only if plasticity feature enabled)
3200    /// The executor is passed as Arc<Mutex<dyn Any>> for feature-gating compatibility
3201    #[cfg(feature = "plasticity")]
3202    pub fn set_plasticity_executor(
3203        &mut self,
3204        executor: Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>,
3205    ) {
3206        self.plasticity_executor = Some(executor);
3207        info!(target: "feagi-bdu", "🔗 ConnectomeManager: PlasticityExecutor reference set");
3208    }
3209
3210    /// Get the PlasticityExecutor reference (if plasticity feature enabled)
3211    #[cfg(feature = "plasticity")]
3212    pub fn get_plasticity_executor(
3213        &self,
3214    ) -> Option<&Arc<std::sync::Mutex<feagi_npu_plasticity::AsyncPlasticityExecutor>>> {
3215        self.plasticity_executor.as_ref()
3216    }
3217
3218    /// Get neuron capacity from config (lock-free, never acquires NPU lock)
3219    ///
3220    /// # Returns
3221    ///
3222    /// * `usize` - Maximum neuron capacity from config (single source of truth)
3223    ///
3224    /// # Performance
3225    ///
3226    /// This is a lock-free read from config that never blocks, even during burst processing.
3227    /// Capacity values are set at NPU initialization and never change.
3228    ///
3229    pub fn get_neuron_capacity(&self) -> usize {
3230        // CRITICAL: Read from config, NOT NPU - capacity never changes and should not acquire locks
3231        self.config.max_neurons
3232    }
3233
3234    /// Get synapse capacity from config (lock-free, never acquires NPU lock)
3235    ///
3236    /// # Returns
3237    ///
3238    /// * `usize` - Maximum synapse capacity from config (single source of truth)
3239    ///
3240    /// # Performance
3241    ///
3242    /// This is a lock-free read from config that never blocks, even during burst processing.
3243    /// Capacity values are set at NPU initialization and never change.
3244    ///
3245    pub fn get_synapse_capacity(&self) -> usize {
3246        // CRITICAL: Read from config, NOT NPU - capacity never changes and should not acquire locks
3247        self.config.max_synapses
3248    }
3249
3250    /// Update fatigue index based on utilization of neuron and synapse arrays
3251    ///
3252    /// Calculates fatigue index as max(regular_neuron_util%, memory_neuron_util%, synapse_util%)
3253    /// Applies hysteresis: triggers at 85%, clears at 80%
3254    /// Rate limited to max once per 2 seconds to protect against rapid changes
3255    ///
3256    /// # Safety
3257    ///
3258    /// This method is completely non-blocking and safe to call during genome loading.
3259    /// If StateManager is unavailable or locked, it will skip the calculation gracefully.
3260    ///
3261    /// # Returns
3262    ///
3263    /// * `Option<u8>` - New fatigue index (0-100) if calculation was performed, None if rate limited or StateManager unavailable
3264    pub fn update_fatigue_index(&self) -> Option<u8> {
3265        // Rate limiting: max once per 2 seconds
3266        let mut last_calc = match self.last_fatigue_calculation.lock() {
3267            Ok(guard) => guard,
3268            Err(_) => return None, // Lock poisoned, skip calculation
3269        };
3270
3271        let now = std::time::Instant::now();
3272        if now.duration_since(*last_calc).as_secs() < 2 {
3273            return None; // Rate limited
3274        }
3275        *last_calc = now;
3276        drop(last_calc);
3277
3278        // Get regular neuron utilization
3279        let regular_neuron_count = self.get_neuron_count();
3280        let regular_neuron_capacity = self.get_neuron_capacity();
3281        let regular_neuron_util = if regular_neuron_capacity > 0 {
3282            ((regular_neuron_count as f64 / regular_neuron_capacity as f64) * 100.0).round() as u8
3283        } else {
3284            0
3285        };
3286
3287        // Get memory neuron utilization from state manager
3288        // Use try_read() to avoid blocking during neurogenesis
3289        // If StateManager singleton initialization fails or is locked, skip calculation entirely
3290        let memory_neuron_util = match StateManager::instance().try_read() {
3291            Some(state_manager) => state_manager.get_core_state().get_memory_neuron_util(),
3292            None => {
3293                // StateManager is locked or not ready - skip fatigue calculation
3294                return None;
3295            }
3296        };
3297
3298        // Get synapse utilization
3299        let synapse_count = self.get_synapse_count();
3300        let synapse_capacity = self.get_synapse_capacity();
3301        let synapse_util = if synapse_capacity > 0 {
3302            ((synapse_count as f64 / synapse_capacity as f64) * 100.0).round() as u8
3303        } else {
3304            0
3305        };
3306
3307        // Calculate fatigue index as max of all utilizations
3308        let fatigue_index = regular_neuron_util
3309            .max(memory_neuron_util)
3310            .max(synapse_util);
3311
3312        // Apply hysteresis: trigger at 85%, clear at 80%
3313        let current_fatigue_active = {
3314            // Try to read current state - if unavailable, assume false
3315            StateManager::instance()
3316                .try_read()
3317                .map(|m| m.get_core_state().is_fatigue_active())
3318                .unwrap_or(false)
3319        };
3320
3321        let new_fatigue_active = if fatigue_index >= 85 {
3322            true
3323        } else if fatigue_index < 80 {
3324            false
3325        } else {
3326            current_fatigue_active // Keep current state in hysteresis zone
3327        };
3328
3329        // Update state manager with all values
3330        // Use try_write() to avoid blocking during neurogenesis
3331        // If StateManager is unavailable, skip update (non-blocking)
3332        if let Some(state_manager) = StateManager::instance().try_write() {
3333            let core_state = state_manager.get_core_state();
3334            core_state.set_fatigue_index(fatigue_index);
3335            core_state.set_fatigue_active(new_fatigue_active);
3336            core_state.set_regular_neuron_util(regular_neuron_util);
3337            core_state.set_memory_neuron_util(memory_neuron_util);
3338            core_state.set_synapse_util(synapse_util);
3339        } else {
3340            // StateManager is locked or not ready - skip update (non-blocking)
3341            trace!(target: "feagi-bdu", "[FATIGUE] StateManager unavailable, skipping update");
3342        }
3343
3344        // Update NPU's atomic boolean
3345        if let Some(ref npu) = self.npu {
3346            if let Ok(mut npu_lock) = npu.lock() {
3347                npu_lock.set_fatigue_active(new_fatigue_active);
3348            }
3349        }
3350
3351        trace!(
3352            target: "feagi-bdu",
3353            "[FATIGUE] Index={}, Active={}, Regular={}%, Memory={}%, Synapse={}%",
3354            fatigue_index, new_fatigue_active, regular_neuron_util, memory_neuron_util, synapse_util
3355        );
3356
3357        Some(fatigue_index)
3358    }
3359
3360    // ======================================================================
3361    // Neuron/Synapse Creation Methods (Delegates to NPU)
3362    // ======================================================================
3363
3364    /// Create neurons for a cortical area
3365    ///
3366    /// This delegates to the NPU's optimized batch creation function.
3367    ///
3368    /// # Arguments
3369    ///
3370    /// * `cortical_id` - Cortical area ID (6-character string)
3371    ///
3372    /// # Returns
3373    ///
3374    /// Number of neurons created
3375    ///
3376    pub fn create_neurons_for_area(&mut self, cortical_id: &CorticalID) -> BduResult<u32> {
3377        // Get cortical area
3378        let area = self
3379            .cortical_areas
3380            .get(cortical_id)
3381            .ok_or_else(|| {
3382                BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
3383            })?
3384            .clone();
3385
3386        // Get cortical index
3387        let cortical_idx = self.cortical_id_to_idx.get(cortical_id).ok_or_else(|| {
3388            BduError::InvalidArea(format!("No index for cortical area {}", cortical_id))
3389        })?;
3390
3391        // Get NPU
3392        let npu = self
3393            .npu
3394            .as_ref()
3395            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3396
3397        // Extract neural parameters from area properties using CorticalAreaExt trait
3398        // This ensures consistent defaults across the codebase
3399        use crate::models::CorticalAreaExt;
3400        let per_voxel_cnt = area.neurons_per_voxel();
3401        let firing_threshold = area.firing_threshold();
3402        let firing_threshold_increment_x = area.firing_threshold_increment_x();
3403        let firing_threshold_increment_y = area.firing_threshold_increment_y();
3404        let firing_threshold_increment_z = area.firing_threshold_increment_z();
3405        // SIMD-friendly encoding: 0.0 means no limit, convert to MAX
3406        let firing_threshold_limit_raw = area.firing_threshold_limit();
3407        let firing_threshold_limit = if firing_threshold_limit_raw == 0.0 {
3408            f32::MAX // SIMD-friendly encoding: MAX = no limit
3409        } else {
3410            firing_threshold_limit_raw
3411        };
3412
3413        // DEBUG: Log the increment values
3414        if firing_threshold_increment_x != 0.0
3415            || firing_threshold_increment_y != 0.0
3416            || firing_threshold_increment_z != 0.0
3417        {
3418            info!(
3419                target: "feagi-bdu",
3420                "🔍 [DEBUG] Area {}: firing_threshold_increment = [{}, {}, {}]",
3421                cortical_id.as_base_64(),
3422                firing_threshold_increment_x,
3423                firing_threshold_increment_y,
3424                firing_threshold_increment_z
3425            );
3426        } else {
3427            // Check if properties exist but are just 0
3428            if area.properties.contains_key("firing_threshold_increment_x")
3429                || area.properties.contains_key("firing_threshold_increment_y")
3430                || area.properties.contains_key("firing_threshold_increment_z")
3431            {
3432                info!(
3433                    target: "feagi-bdu",
3434                    "🔍 [DEBUG] Area {}: INCREMENT PROPERTIES FOUND: x={:?}, y={:?}, z={:?}",
3435                    cortical_id.as_base_64(),
3436                    area.properties.get("firing_threshold_increment_x"),
3437                    area.properties.get("firing_threshold_increment_y"),
3438                    area.properties.get("firing_threshold_increment_z")
3439                );
3440            }
3441        }
3442
3443        let leak_coefficient = area.leak_coefficient();
3444        let excitability = area.neuron_excitability();
3445        let refractory_period = area.refractory_period();
3446        // SIMD-friendly encoding: 0 means no limit, convert to MAX
3447        let consecutive_fire_limit_raw = area.consecutive_fire_count() as u16;
3448        let consecutive_fire_limit = if consecutive_fire_limit_raw == 0 {
3449            u16::MAX // SIMD-friendly encoding: MAX = no limit
3450        } else {
3451            consecutive_fire_limit_raw
3452        };
3453        let snooze_length = area.snooze_period();
3454        let mp_charge_accumulation = area.mp_charge_accumulation();
3455
3456        // Calculate expected neuron count for logging
3457        let voxels = area.dimensions.width as usize
3458            * area.dimensions.height as usize
3459            * area.dimensions.depth as usize;
3460        let expected_neurons = voxels * per_voxel_cnt as usize;
3461
3462        trace!(
3463            target: "feagi-bdu",
3464            "Creating neurons for area {}: {}x{}x{} voxels × {} neurons/voxel = {} total neurons",
3465            cortical_id.as_base_64(),
3466            area.dimensions.width,
3467            area.dimensions.height,
3468            area.dimensions.depth,
3469            per_voxel_cnt,
3470            expected_neurons
3471        );
3472
3473        // Call NPU to create neurons
3474        // NOTE: Cortical area should already be registered in NPU during corticogenesis
3475        let mut npu_lock = npu
3476            .lock()
3477            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3478
3479        let neuron_count = npu_lock
3480            .create_cortical_area_neurons(
3481                *cortical_idx,
3482                area.dimensions.width,
3483                area.dimensions.height,
3484                area.dimensions.depth,
3485                per_voxel_cnt,
3486                firing_threshold,
3487                firing_threshold_increment_x,
3488                firing_threshold_increment_y,
3489                firing_threshold_increment_z,
3490                firing_threshold_limit,
3491                leak_coefficient,
3492                0.0, // resting_potential (LIF default)
3493                0,   // neuron_type (excitatory)
3494                refractory_period,
3495                excitability,
3496                consecutive_fire_limit,
3497                snooze_length,
3498                mp_charge_accumulation,
3499            )
3500            .map_err(|e| BduError::Internal(format!("NPU neuron creation failed: {}", e)))?;
3501
3502        trace!(
3503            target: "feagi-bdu",
3504            "Created {} neurons for area {} via NPU",
3505            neuron_count,
3506            cortical_id.as_base_64()
3507        );
3508
3509        // CRITICAL: Update per-area neuron count cache (lock-free for readers)
3510        // This allows healthcheck endpoints to read counts without NPU lock
3511        {
3512            let mut cache = self.cached_neuron_counts_per_area.write();
3513            cache
3514                .entry(*cortical_id)
3515                .or_insert_with(|| AtomicUsize::new(0))
3516                .store(neuron_count as usize, Ordering::Relaxed);
3517        }
3518
3519        // @cursor:critical-path - Keep BV-facing stats in StateManager.
3520        if let Some(state_manager) = StateManager::instance().try_read() {
3521            state_manager
3522                .set_cortical_area_neuron_count(&cortical_id.as_base_64(), neuron_count as usize);
3523        }
3524
3525        // Update total neuron count cache
3526        self.cached_neuron_count
3527            .fetch_add(neuron_count as usize, Ordering::Relaxed);
3528
3529        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3530        if let Some(state_manager) = StateManager::instance().try_read() {
3531            let core_state = state_manager.get_core_state();
3532            core_state.add_neuron_count(neuron_count);
3533            core_state.add_regular_neuron_count(neuron_count);
3534        }
3535
3536        // Trigger fatigue index recalculation after neuron creation
3537        // NOTE: Disabled during genome loading to prevent blocking
3538        // Fatigue calculation will be enabled after genome loading completes
3539        // if neuron_count > 0 {
3540        //     let _ = self.update_fatigue_index();
3541        // }
3542
3543        Ok(neuron_count)
3544    }
3545
3546    /// Add a single neuron to a cortical area
3547    ///
3548    /// # Arguments
3549    ///
3550    /// * `cortical_id` - Cortical area ID
3551    /// * `x` - X coordinate
3552    /// * `y` - Y coordinate
3553    /// * `z` - Z coordinate
3554    /// * `firing_threshold` - Firing threshold (minimum MP to fire)
3555    /// * `firing_threshold_limit` - Firing threshold limit (maximum MP to fire, 0 = no limit)
3556    /// * `leak_coefficient` - Leak coefficient
3557    /// * `resting_potential` - Resting membrane potential
3558    /// * `neuron_type` - Neuron type (0=excitatory, 1=inhibitory)
3559    /// * `refractory_period` - Refractory period
3560    /// * `excitability` - Excitability multiplier
3561    /// * `consecutive_fire_limit` - Maximum consecutive fires
3562    /// * `snooze_length` - Snooze duration after consecutive fire limit
3563    /// * `mp_charge_accumulation` - Whether membrane potential accumulates
3564    ///
3565    /// # Returns
3566    ///
3567    /// The newly created neuron ID
3568    ///
3569    #[allow(clippy::too_many_arguments)]
3570    pub fn add_neuron(
3571        &mut self,
3572        cortical_id: &CorticalID,
3573        x: u32,
3574        y: u32,
3575        z: u32,
3576        firing_threshold: f32,
3577        firing_threshold_limit: f32,
3578        leak_coefficient: f32,
3579        resting_potential: f32,
3580        neuron_type: u8,
3581        refractory_period: u16,
3582        excitability: f32,
3583        consecutive_fire_limit: u16,
3584        snooze_length: u16,
3585        mp_charge_accumulation: bool,
3586    ) -> BduResult<u64> {
3587        // Validate cortical area exists
3588        if !self.cortical_areas.contains_key(cortical_id) {
3589            return Err(BduError::InvalidArea(format!(
3590                "Cortical area {} not found",
3591                cortical_id
3592            )));
3593        }
3594
3595        let cortical_idx = *self
3596            .cortical_id_to_idx
3597            .get(cortical_id)
3598            .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", cortical_id)))?;
3599
3600        // Get NPU
3601        let npu = self
3602            .npu
3603            .as_ref()
3604            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3605
3606        let mut npu_lock = npu
3607            .lock()
3608            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3609
3610        // Add neuron via NPU
3611        let neuron_id = npu_lock
3612            .add_neuron(
3613                firing_threshold,
3614                firing_threshold_limit,
3615                leak_coefficient,
3616                resting_potential,
3617                neuron_type as i32,
3618                refractory_period,
3619                excitability,
3620                consecutive_fire_limit,
3621                snooze_length,
3622                mp_charge_accumulation,
3623                cortical_idx,
3624                x,
3625                y,
3626                z,
3627            )
3628            .map_err(|e| BduError::Internal(format!("Failed to add neuron: {}", e)))?;
3629
3630        trace!(
3631            target: "feagi-bdu",
3632            "Created neuron {} in area {} at ({}, {}, {})",
3633            neuron_id.0,
3634            cortical_id,
3635            x,
3636            y,
3637            z
3638        );
3639
3640        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3641        if let Some(state_manager) = StateManager::instance().try_read() {
3642            let core_state = state_manager.get_core_state();
3643            core_state.add_neuron_count(1);
3644            core_state.add_regular_neuron_count(1);
3645            state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
3646        }
3647
3648        Ok(neuron_id.0 as u64)
3649    }
3650
3651    /// Delete a neuron by ID
3652    ///
3653    /// # Arguments
3654    ///
3655    /// * `neuron_id` - Global neuron ID
3656    ///
3657    /// # Returns
3658    ///
3659    /// `true` if the neuron was deleted, `false` if it didn't exist
3660    ///
3661    pub fn delete_neuron(&mut self, neuron_id: u64) -> BduResult<bool> {
3662        // Get NPU
3663        let npu = self
3664            .npu
3665            .as_ref()
3666            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
3667
3668        let mut npu_lock = npu
3669            .lock()
3670            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
3671
3672        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
3673        let cortical_id = self.cortical_idx_to_id.get(&cortical_idx).cloned();
3674
3675        let deleted = npu_lock.delete_neuron(neuron_id as u32);
3676
3677        if deleted {
3678            trace!(target: "feagi-bdu", "Deleted neuron {}", neuron_id);
3679
3680            // CRITICAL: Update StateManager neuron count (for health_check endpoint)
3681            if let Some(state_manager) = StateManager::instance().try_read() {
3682                let core_state = state_manager.get_core_state();
3683                core_state.subtract_neuron_count(1);
3684                core_state.subtract_regular_neuron_count(1);
3685                if let Some(cortical_id) = cortical_id {
3686                    state_manager.subtract_cortical_area_neuron_count(&cortical_id.as_base_64(), 1);
3687                }
3688            }
3689
3690            // Trigger fatigue index recalculation after neuron deletion
3691            // NOTE: Disabled during genome loading to prevent blocking
3692            // let _ = self.update_fatigue_index();
3693        }
3694
3695        Ok(deleted)
3696    }
3697
3698    /// Apply cortical mapping rules (dstmap) to create synapses
3699    ///
3700    /// This parses the destination mapping rules from a source area and
3701    /// creates synapses using the NPU's synaptogenesis functions.
3702    ///
3703    /// # Arguments
3704    ///
3705    /// * `src_cortical_id` - Source cortical area ID
3706    ///
3707    /// # Returns
3708    ///
3709    /// Number of synapses created
3710    ///
3711    pub fn apply_cortical_mapping(&mut self, src_cortical_id: &CorticalID) -> BduResult<u32> {
3712        // Get source area
3713        let src_area = self
3714            .cortical_areas
3715            .get(src_cortical_id)
3716            .ok_or_else(|| {
3717                BduError::InvalidArea(format!("Source area {} not found", src_cortical_id))
3718            })?
3719            .clone();
3720
3721        // Get dstmap from area properties
3722        let dstmap = match src_area.properties.get("cortical_mapping_dst") {
3723            Some(serde_json::Value::Object(map)) if !map.is_empty() => map,
3724            _ => return Ok(0), // No mappings
3725        };
3726
3727        let src_cortical_idx = *self
3728            .cortical_id_to_idx
3729            .get(src_cortical_id)
3730            .ok_or_else(|| BduError::InvalidArea(format!("No index for {}", src_cortical_id)))?;
3731
3732        let mut total_synapses = 0u32;
3733        let mut upstream_updates: Vec<(CorticalID, u32)> = Vec::new(); // Collect updates to apply later
3734
3735        // Process each destination area using the unified path
3736        for (dst_cortical_id_str, _rules) in dstmap {
3737            // Convert string to CorticalID
3738            let dst_cortical_id = match CorticalID::try_from_base_64(dst_cortical_id_str) {
3739                Ok(id) => id,
3740                Err(_) => {
3741                    warn!(target: "feagi-bdu","Invalid cortical ID format: {}, skipping", dst_cortical_id_str);
3742                    continue;
3743                }
3744            };
3745
3746            // Verify destination area exists
3747            if !self.cortical_id_to_idx.contains_key(&dst_cortical_id) {
3748                warn!(target: "feagi-bdu","Destination area {} not found, skipping", dst_cortical_id);
3749                continue;
3750            }
3751
3752            // Apply cortical mapping for this pair (handles STDP and all morphology rules)
3753            let synapse_count =
3754                self.apply_cortical_mapping_for_pair(src_cortical_id, &dst_cortical_id)?;
3755            total_synapses += synapse_count as u32;
3756
3757            // Queue upstream area update for ANY mapping (even if no synapses created)
3758            // This is critical for memory areas which have mappings but no physical synapses
3759            upstream_updates.push((dst_cortical_id, src_cortical_idx));
3760        }
3761
3762        // Apply all upstream area updates now that NPU borrows are complete
3763        for (dst_id, src_idx) in upstream_updates {
3764            self.add_upstream_area(&dst_id, src_idx);
3765        }
3766
3767        trace!(
3768            target: "feagi-bdu",
3769            "Created {} synapses for area {} via NPU",
3770            total_synapses,
3771            src_cortical_id
3772        );
3773
3774        // CRITICAL: Update per-area synapse count cache (lock-free for readers)
3775        // This allows healthcheck endpoints to read counts without NPU lock
3776        if total_synapses > 0 {
3777            let mut cache = self.cached_synapse_counts_per_area.write();
3778            cache
3779                .entry(*src_cortical_id)
3780                .or_insert_with(|| AtomicUsize::new(0))
3781                .fetch_add(total_synapses as usize, Ordering::Relaxed);
3782        }
3783
3784        // Update total synapse count cache
3785        self.cached_synapse_count
3786            .fetch_add(total_synapses as usize, Ordering::Relaxed);
3787
3788        // CRITICAL: Update StateManager synapse count (for health_check endpoint)
3789        if total_synapses > 0 {
3790            if let Some(state_manager) = StateManager::instance().try_read() {
3791                let core_state = state_manager.get_core_state();
3792                core_state.add_synapse_count(total_synapses);
3793            }
3794        }
3795
3796        Ok(total_synapses)
3797    }
3798
3799    // ======================================================================
3800    // Neuron Query Methods (Delegates to NPU)
3801    // ======================================================================
3802
3803    /// Check if a neuron exists
3804    ///
3805    /// # Arguments
3806    ///
3807    /// * `neuron_id` - The neuron ID to check
3808    ///
3809    /// # Returns
3810    ///
3811    /// `true` if the neuron exists in the NPU, `false` otherwise
3812    ///
3813    /// # Note
3814    ///
3815    /// Returns `false` if NPU is not connected
3816    ///
3817    pub fn has_neuron(&self, neuron_id: u64) -> bool {
3818        if let Some(ref npu) = self.npu {
3819            if let Ok(npu_lock) = npu.lock() {
3820                // Check if neuron exists AND is valid (not deleted)
3821                npu_lock.is_neuron_valid(neuron_id as u32)
3822            } else {
3823                false
3824            }
3825        } else {
3826            false
3827        }
3828    }
3829
3830    /// Get total number of active neurons (lock-free cached read with opportunistic update)
3831    ///
3832    /// # Returns
3833    ///
3834    /// The total number of neurons (from cache)
3835    ///
3836    /// # Performance
3837    ///
3838    /// This is a lock-free atomic read that never blocks, even during burst processing.
3839    /// Opportunistically updates cache if NPU is available (non-blocking try_lock).
3840    ///
3841    pub fn get_neuron_count(&self) -> usize {
3842        // Opportunistically update cache if NPU is available (non-blocking)
3843        if let Some(ref npu) = self.npu {
3844            if let Ok(npu_lock) = npu.try_lock() {
3845                let fresh_count = npu_lock.get_neuron_count();
3846                self.cached_neuron_count
3847                    .store(fresh_count, Ordering::Relaxed);
3848            }
3849            // If NPU is busy, just use cached value
3850        }
3851
3852        // Always return cached value (never blocks)
3853        self.cached_neuron_count.load(Ordering::Relaxed)
3854    }
3855
3856    /// Update the cached neuron count (explicit update)
3857    ///
3858    /// Use this if you want to force a cache update. Most callers should just
3859    /// use get_neuron_count() which updates opportunistically.
3860    ///
3861    pub fn update_cached_neuron_count(&self) {
3862        if let Some(ref npu) = self.npu {
3863            if let Ok(npu_lock) = npu.try_lock() {
3864                let count = npu_lock.get_neuron_count();
3865                self.cached_neuron_count.store(count, Ordering::Relaxed);
3866            }
3867        }
3868    }
3869
3870    /// Refresh cached neuron count for a single cortical area from the NPU.
3871    ///
3872    /// Returns the refreshed count if successful.
3873    pub fn refresh_neuron_count_for_area(&self, cortical_id: &CorticalID) -> Option<usize> {
3874        let npu = self.npu.as_ref()?;
3875        let cortical_idx = *self.cortical_id_to_idx.get(cortical_id)?;
3876        let npu_lock = npu.lock().ok()?;
3877        let count = npu_lock.get_neurons_in_cortical_area(cortical_idx).len();
3878        drop(npu_lock);
3879
3880        let mut cache = self.cached_neuron_counts_per_area.write();
3881        cache
3882            .entry(*cortical_id)
3883            .or_insert_with(|| AtomicUsize::new(0))
3884            .store(count, Ordering::Relaxed);
3885
3886        // @cursor:critical-path - Keep BV-facing stats in StateManager.
3887        if let Some(state_manager) = StateManager::instance().try_read() {
3888            state_manager.set_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
3889        }
3890
3891        self.update_cached_neuron_count();
3892
3893        Some(count)
3894    }
3895
3896    /// Get total number of synapses (lock-free cached read with opportunistic update)
3897    ///
3898    /// # Returns
3899    ///
3900    /// The total number of synapses (from cache)
3901    ///
3902    /// # Performance
3903    ///
3904    /// This is a lock-free atomic read that never blocks, even during burst processing.
3905    /// Opportunistically updates cache if NPU is available (non-blocking try_lock).
3906    ///
3907    pub fn get_synapse_count(&self) -> usize {
3908        // Opportunistically update cache if NPU is available (non-blocking)
3909        if let Some(ref npu) = self.npu {
3910            if let Ok(npu_lock) = npu.try_lock() {
3911                let fresh_count = npu_lock.get_synapse_count();
3912                self.cached_synapse_count
3913                    .store(fresh_count, Ordering::Relaxed);
3914            }
3915            // If NPU is busy, just use cached value
3916        }
3917
3918        // Always return cached value (never blocks)
3919        self.cached_synapse_count.load(Ordering::Relaxed)
3920    }
3921
3922    /// Update the cached synapse count (explicit update)
3923    ///
3924    /// Use this if you want to force a cache update. Most callers should just
3925    /// use get_synapse_count() which updates opportunistically.
3926    ///
3927    pub fn update_cached_synapse_count(&self) {
3928        if let Some(ref npu) = self.npu {
3929            if let Ok(npu_lock) = npu.try_lock() {
3930                let count = npu_lock.get_synapse_count();
3931                self.cached_synapse_count.store(count, Ordering::Relaxed);
3932            }
3933        }
3934    }
3935
3936    /// Update all cached stats (neuron and synapse counts)
3937    ///
3938    /// This is called automatically when NPU is connected and can be called
3939    /// explicitly if you want to force a cache refresh.
3940    ///
3941    pub fn update_all_cached_stats(&self) {
3942        self.update_cached_neuron_count();
3943        self.update_cached_synapse_count();
3944    }
3945
3946    /// Get neuron coordinates (x, y, z)
3947    ///
3948    /// # Arguments
3949    ///
3950    /// * `neuron_id` - The neuron ID to query
3951    ///
3952    /// # Returns
3953    ///
3954    /// Coordinates as (x, y, z), or (0, 0, 0) if neuron doesn't exist or NPU not connected
3955    ///
3956    pub fn get_neuron_coordinates(&self, neuron_id: u64) -> (u32, u32, u32) {
3957        if let Some(ref npu) = self.npu {
3958            if let Ok(npu_lock) = npu.lock() {
3959                npu_lock
3960                    .get_neuron_coordinates(neuron_id as u32)
3961                    .unwrap_or((0, 0, 0))
3962            } else {
3963                (0, 0, 0)
3964            }
3965        } else {
3966            (0, 0, 0)
3967        }
3968    }
3969
3970    /// Get the cortical area index for a neuron
3971    ///
3972    /// # Arguments
3973    ///
3974    /// * `neuron_id` - The neuron ID to query
3975    ///
3976    /// # Returns
3977    ///
3978    /// Cortical area index, or 0 if neuron doesn't exist or NPU not connected
3979    ///
3980    pub fn get_neuron_cortical_idx(&self, neuron_id: u64) -> u32 {
3981        if let Some(ref npu) = self.npu {
3982            if let Ok(npu_lock) = npu.lock() {
3983                npu_lock.get_neuron_cortical_area(neuron_id as u32)
3984            } else {
3985                0
3986            }
3987        } else {
3988            0
3989        }
3990    }
3991
3992    /// Get all neuron IDs in a specific cortical area
3993    ///
3994    /// # Arguments
3995    ///
3996    /// * `cortical_id` - The cortical area ID (string)
3997    ///
3998    /// # Returns
3999    ///
4000    /// Vec of neuron IDs in the area, or empty vec if area doesn't exist or NPU not connected
4001    ///
4002    pub fn get_neurons_in_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
4003        // Get cortical_idx from cortical_id
4004        let cortical_idx = match self.cortical_id_to_idx.get(cortical_id) {
4005            Some(idx) => *idx,
4006            None => return Vec::new(),
4007        };
4008
4009        if let Some(ref npu) = self.npu {
4010            if let Ok(npu_lock) = npu.lock() {
4011                // Convert Vec<u32> to Vec<u64>
4012                npu_lock
4013                    .get_neurons_in_cortical_area(cortical_idx)
4014                    .into_iter()
4015                    .map(|id| id as u64)
4016                    .collect()
4017            } else {
4018                Vec::new()
4019            }
4020        } else {
4021            Vec::new()
4022        }
4023    }
4024
4025    /// Get all outgoing synapses from a source neuron
4026    ///
4027    /// # Arguments
4028    ///
4029    /// * `source_neuron_id` - The source neuron ID
4030    ///
4031    /// # Returns
4032    ///
4033    /// Vec of (target_neuron_id, weight, psp, synapse_type), or empty if NPU not connected
4034    ///
4035    pub fn get_outgoing_synapses(&self, source_neuron_id: u64) -> Vec<(u32, u8, u8, u8)> {
4036        if let Some(ref npu) = self.npu {
4037            if let Ok(npu_lock) = npu.lock() {
4038                npu_lock.get_outgoing_synapses(source_neuron_id as u32)
4039            } else {
4040                Vec::new()
4041            }
4042        } else {
4043            Vec::new()
4044        }
4045    }
4046
4047    /// Get all incoming synapses to a target neuron
4048    ///
4049    /// # Arguments
4050    ///
4051    /// * `target_neuron_id` - The target neuron ID
4052    ///
4053    /// # Returns
4054    ///
4055    /// Vec of (source_neuron_id, weight, psp, synapse_type), or empty if NPU not connected
4056    ///
4057    pub fn get_incoming_synapses(&self, target_neuron_id: u64) -> Vec<(u32, u8, u8, u8)> {
4058        if let Some(ref npu) = self.npu {
4059            if let Ok(npu_lock) = npu.lock() {
4060                npu_lock.get_incoming_synapses(target_neuron_id as u32)
4061            } else {
4062                Vec::new()
4063            }
4064        } else {
4065            Vec::new()
4066        }
4067    }
4068
4069    /// Get neuron count for a specific cortical area
4070    ///
4071    /// # Arguments
4072    ///
4073    /// * `cortical_id` - The cortical area ID (string)
4074    ///
4075    /// # Returns
4076    ///
4077    /// Number of neurons in the area, or 0 if area doesn't exist or NPU not connected
4078    ///
4079    /// Get neuron count for a specific cortical area (lock-free cached read)
4080    ///
4081    /// # Arguments
4082    ///
4083    /// * `cortical_id` - The cortical area ID
4084    ///
4085    /// # Returns
4086    ///
4087    /// The number of neurons in the area (from cache, never blocks on NPU lock)
4088    ///
4089    /// # Performance
4090    ///
4091    /// This is a lock-free atomic read that never blocks, even during burst processing.
4092    /// Count is maintained in ConnectomeManager and updated when neurons are created/deleted.
4093    ///
4094    pub fn get_neuron_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4095        // CRITICAL: Read from cache (lock-free) - never query NPU for healthcheck endpoints
4096        let cache = self.cached_neuron_counts_per_area.read();
4097        let base_count = cache
4098            .get(cortical_id)
4099            .map(|count| count.load(Ordering::Relaxed))
4100            .unwrap_or(0);
4101
4102        // Memory areas maintain neurons outside the NPU; add their count from StateManager.
4103        let memory_count = self
4104            .cortical_areas
4105            .get(cortical_id)
4106            .and_then(|area| feagi_evolutionary::extract_memory_properties(&area.properties))
4107            .and_then(|_| {
4108                StateManager::instance()
4109                    .try_read()
4110                    .and_then(|state_manager| {
4111                        state_manager.get_cortical_area_stats(&cortical_id.as_base_64())
4112                    })
4113            })
4114            .map(|stats| stats.neuron_count)
4115            .unwrap_or(0);
4116
4117        base_count.saturating_add(memory_count)
4118    }
4119
4120    /// Get all cortical areas that have neurons
4121    ///
4122    /// # Returns
4123    ///
4124    /// Vec of (cortical_id, neuron_count) for areas with at least one neuron
4125    ///
4126    pub fn get_populated_areas(&self) -> Vec<(String, usize)> {
4127        let mut result = Vec::new();
4128
4129        for cortical_id in self.cortical_areas.keys() {
4130            let count = self.get_neuron_count_in_area(cortical_id);
4131            if count > 0 {
4132                result.push((cortical_id.to_string(), count));
4133            }
4134        }
4135
4136        result
4137    }
4138
4139    /// Check if a cortical area has any neurons
4140    ///
4141    /// # Arguments
4142    ///
4143    /// * `cortical_id` - The cortical area ID
4144    ///
4145    /// # Returns
4146    ///
4147    /// `true` if the area has at least one neuron, `false` otherwise
4148    ///
4149    pub fn is_area_populated(&self, cortical_id: &CorticalID) -> bool {
4150        self.get_neuron_count_in_area(cortical_id) > 0
4151    }
4152
4153    /// Get total synapse count for a specific cortical area (outgoing only) - lock-free cached read
4154    ///
4155    /// # Arguments
4156    ///
4157    /// * `cortical_id` - The cortical area ID
4158    ///
4159    /// # Returns
4160    ///
4161    /// Total number of outgoing synapses from neurons in this area (from cache, never blocks on NPU lock)
4162    ///
4163    /// # Performance
4164    ///
4165    /// This is a lock-free atomic read that never blocks, even during burst processing.
4166    /// Count is maintained in ConnectomeManager and updated when synapses are created/deleted.
4167    ///
4168    pub fn get_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4169        // CRITICAL: Read from cache (lock-free) - never query NPU for healthcheck endpoints
4170        let cache = self.cached_synapse_counts_per_area.read();
4171        cache
4172            .get(cortical_id)
4173            .map(|count| count.load(Ordering::Relaxed))
4174            .unwrap_or(0)
4175    }
4176
4177    /// Get total incoming synapse count for a specific cortical area.
4178    ///
4179    /// # Arguments
4180    ///
4181    /// * `cortical_id` - The cortical area ID
4182    ///
4183    /// # Returns
4184    ///
4185    /// Total number of incoming synapses targeting neurons in this area.
4186    pub fn get_incoming_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4187        if !self.cortical_id_to_idx.contains_key(cortical_id) {
4188            return 0;
4189        }
4190
4191        if let Some(state_manager) = StateManager::instance().try_read() {
4192            if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
4193                return stats.incoming_synapse_count;
4194            }
4195        }
4196
4197        0
4198    }
4199
4200    /// Get total outgoing synapse count for a specific cortical area.
4201    ///
4202    /// # Arguments
4203    ///
4204    /// * `cortical_id` - The cortical area ID
4205    ///
4206    /// # Returns
4207    ///
4208    /// Total number of outgoing synapses originating from neurons in this area.
4209    pub fn get_outgoing_synapse_count_in_area(&self, cortical_id: &CorticalID) -> usize {
4210        if !self.cortical_id_to_idx.contains_key(cortical_id) {
4211            return 0;
4212        }
4213
4214        if let Some(state_manager) = StateManager::instance().try_read() {
4215            if let Some(stats) = state_manager.get_cortical_area_stats(&cortical_id.as_base_64()) {
4216                return stats.outgoing_synapse_count;
4217            }
4218        }
4219
4220        0
4221    }
4222
4223    /// Check if two neurons are connected (source → target)
4224    ///
4225    /// # Arguments
4226    ///
4227    /// * `source_neuron_id` - The source neuron ID
4228    /// * `target_neuron_id` - The target neuron ID
4229    ///
4230    /// # Returns
4231    ///
4232    /// `true` if there is a synapse from source to target, `false` otherwise
4233    ///
4234    pub fn are_neurons_connected(&self, source_neuron_id: u64, target_neuron_id: u64) -> bool {
4235        let synapses = self.get_outgoing_synapses(source_neuron_id);
4236        synapses
4237            .iter()
4238            .any(|(target, _, _, _)| *target == target_neuron_id as u32)
4239    }
4240
4241    /// Get connection strength (weight) between two neurons
4242    ///
4243    /// # Arguments
4244    ///
4245    /// * `source_neuron_id` - The source neuron ID
4246    /// * `target_neuron_id` - The target neuron ID
4247    ///
4248    /// # Returns
4249    ///
4250    /// Synapse weight (0-255), or None if no connection exists
4251    ///
4252    pub fn get_connection_weight(
4253        &self,
4254        source_neuron_id: u64,
4255        target_neuron_id: u64,
4256    ) -> Option<u8> {
4257        let synapses = self.get_outgoing_synapses(source_neuron_id);
4258        synapses
4259            .iter()
4260            .find(|(target, _, _, _)| *target == target_neuron_id as u32)
4261            .map(|(_, weight, _, _)| *weight)
4262    }
4263
4264    /// Get connectivity statistics for a cortical area
4265    ///
4266    /// # Arguments
4267    ///
4268    /// * `cortical_id` - The cortical area ID
4269    ///
4270    /// # Returns
4271    ///
4272    /// (neuron_count, total_synapses, avg_synapses_per_neuron)
4273    ///
4274    pub fn get_area_connectivity_stats(&self, cortical_id: &CorticalID) -> (usize, usize, f32) {
4275        let neurons = self.get_neurons_in_area(cortical_id);
4276        let neuron_count = neurons.len();
4277
4278        if neuron_count == 0 {
4279            return (0, 0, 0.0);
4280        }
4281
4282        let mut total_synapses = 0;
4283        for neuron_id in neurons {
4284            total_synapses += self.get_outgoing_synapses(neuron_id).len();
4285        }
4286
4287        let avg_synapses = total_synapses as f32 / neuron_count as f32;
4288
4289        (neuron_count, total_synapses, avg_synapses)
4290    }
4291
4292    /// Get the cortical area ID (string) for a neuron
4293    ///
4294    /// # Arguments
4295    ///
4296    /// * `neuron_id` - The neuron ID
4297    ///
4298    /// # Returns
4299    ///
4300    /// The cortical area ID, or None if neuron doesn't exist
4301    ///
4302    pub fn get_neuron_cortical_id(&self, neuron_id: u64) -> Option<CorticalID> {
4303        let cortical_idx = self.get_neuron_cortical_idx(neuron_id);
4304        self.cortical_idx_to_id.get(&cortical_idx).copied()
4305    }
4306
4307    /// Get neuron density (neurons per voxel) for a cortical area
4308    ///
4309    /// # Arguments
4310    ///
4311    /// * `cortical_id` - The cortical area ID
4312    ///
4313    /// # Returns
4314    ///
4315    /// Neuron density (neurons per voxel), or 0.0 if area doesn't exist
4316    ///
4317    pub fn get_neuron_density(&self, cortical_id: &CorticalID) -> f32 {
4318        let area = match self.cortical_areas.get(cortical_id) {
4319            Some(a) => a,
4320            None => return 0.0,
4321        };
4322
4323        let neuron_count = self.get_neuron_count_in_area(cortical_id);
4324        let volume = area.dimensions.width * area.dimensions.height * area.dimensions.depth;
4325
4326        if volume == 0 {
4327            return 0.0;
4328        }
4329
4330        neuron_count as f32 / volume as f32
4331    }
4332
4333    /// Get all cortical areas with connectivity statistics
4334    ///
4335    /// # Returns
4336    ///
4337    /// Vec of (cortical_id, neuron_count, synapse_count, density)
4338    ///
4339    pub fn get_all_area_stats(&self) -> Vec<(String, usize, usize, f32)> {
4340        let mut stats = Vec::new();
4341
4342        for cortical_id in self.cortical_areas.keys() {
4343            let neuron_count = self.get_neuron_count_in_area(cortical_id);
4344            let synapse_count = self.get_synapse_count_in_area(cortical_id);
4345            let density = self.get_neuron_density(cortical_id);
4346
4347            stats.push((
4348                cortical_id.to_string(),
4349                neuron_count,
4350                synapse_count,
4351                density,
4352            ));
4353        }
4354
4355        stats
4356    }
4357
4358    // ======================================================================
4359    // Configuration
4360    // ======================================================================
4361
4362    /// Get the configuration
4363    pub fn get_config(&self) -> &ConnectomeConfig {
4364        &self.config
4365    }
4366
4367    /// Update configuration
4368    pub fn set_config(&mut self, config: ConnectomeConfig) {
4369        self.config = config;
4370    }
4371
4372    // ======================================================================
4373    // Genome I/O
4374    // ======================================================================
4375
4376    /// Ensure core cortical areas (_death, _power, _fatigue) exist
4377    ///
4378    /// Core areas are required for brain operation:
4379    /// - `_death` (cortical_idx=0): Manages neuron death and cleanup
4380    /// - `_power` (cortical_idx=1): Provides power injection for burst engine
4381    /// - `_fatigue` (cortical_idx=2): Monitors brain fatigue and triggers sleep mode
4382    ///
4383    /// If any core area is missing from the genome, it will be automatically created
4384    /// with default properties (1x1x1 dimensions, minimal configuration).
4385    ///
4386    /// # Returns
4387    ///
4388    /// * `Ok(())` if all core areas exist or were successfully created
4389    /// * `Err(BduError)` if creation fails
4390    pub fn ensure_core_cortical_areas(&mut self) -> BduResult<()> {
4391        info!(target: "feagi-bdu", "🔧 [CORE-AREA] Ensuring core cortical areas exist...");
4392
4393        use feagi_structures::genomic::cortical_area::{
4394            CoreCorticalType, CorticalArea, CorticalAreaDimensions, CorticalAreaType,
4395        };
4396
4397        // Core areas are always 1x1x1 as per requirements
4398        let core_dimensions = CorticalAreaDimensions::new(1, 1, 1).map_err(|e| {
4399            BduError::Internal(format!("Failed to create core area dimensions: {}", e))
4400        })?;
4401
4402        // Default position for core areas (origin)
4403        let core_position = (0, 0, 0).into();
4404
4405        // Check and create _death (cortical_idx=0)
4406        let death_id = CoreCorticalType::Death.to_cortical_id();
4407        if !self.cortical_areas.contains_key(&death_id) {
4408            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _death area (cortical_idx=0)");
4409            let death_area = CorticalArea::new(
4410                death_id,
4411                0, // Will be overridden by add_cortical_area to 0
4412                "_death".to_string(),
4413                core_dimensions,
4414                core_position,
4415                CorticalAreaType::Core(CoreCorticalType::Death),
4416            )
4417            .map_err(|e| BduError::Internal(format!("Failed to create _death area: {}", e)))?;
4418            match self.add_cortical_area(death_area) {
4419                Ok(idx) => {
4420                    info!(target: "feagi-bdu", "  ✅ Created _death area with cortical_idx={}", idx);
4421                }
4422                Err(e) => {
4423                    error!(target: "feagi-bdu", "  ❌ Failed to add _death area: {}", e);
4424                    return Err(e);
4425                }
4426            }
4427        } else {
4428            info!(target: "feagi-bdu", "  ✓ _death area already exists");
4429        }
4430
4431        // Check and create _power (cortical_idx=1)
4432        let power_id = CoreCorticalType::Power.to_cortical_id();
4433        if !self.cortical_areas.contains_key(&power_id) {
4434            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _power area (cortical_idx=1)");
4435            let power_area = CorticalArea::new(
4436                power_id,
4437                1, // Will be overridden by add_cortical_area to 1
4438                "_power".to_string(),
4439                core_dimensions,
4440                core_position,
4441                CorticalAreaType::Core(CoreCorticalType::Power),
4442            )
4443            .map_err(|e| BduError::Internal(format!("Failed to create _power area: {}", e)))?;
4444            match self.add_cortical_area(power_area) {
4445                Ok(idx) => {
4446                    info!(target: "feagi-bdu", "  ✅ Created _power area with cortical_idx={}", idx);
4447                }
4448                Err(e) => {
4449                    error!(target: "feagi-bdu", "  ❌ Failed to add _power area: {}", e);
4450                    return Err(e);
4451                }
4452            }
4453        } else {
4454            info!(target: "feagi-bdu", "  ✓ _power area already exists");
4455        }
4456
4457        // Check and create _fatigue (cortical_idx=2)
4458        let fatigue_id = CoreCorticalType::Fatigue.to_cortical_id();
4459        if !self.cortical_areas.contains_key(&fatigue_id) {
4460            info!(target: "feagi-bdu", "🔧 [CORE-AREA] Creating missing _fatigue area (cortical_idx=2)");
4461            let fatigue_area = CorticalArea::new(
4462                fatigue_id,
4463                2, // Will be overridden by add_cortical_area to 2
4464                "_fatigue".to_string(),
4465                core_dimensions,
4466                core_position,
4467                CorticalAreaType::Core(CoreCorticalType::Fatigue),
4468            )
4469            .map_err(|e| BduError::Internal(format!("Failed to create _fatigue area: {}", e)))?;
4470            match self.add_cortical_area(fatigue_area) {
4471                Ok(idx) => {
4472                    info!(target: "feagi-bdu", "  ✅ Created _fatigue area with cortical_idx={}", idx);
4473                }
4474                Err(e) => {
4475                    error!(target: "feagi-bdu", "  ❌ Failed to add _fatigue area: {}", e);
4476                    return Err(e);
4477                }
4478            }
4479        } else {
4480            info!(target: "feagi-bdu", "  ✓ _fatigue area already exists");
4481        }
4482
4483        info!(target: "feagi-bdu", "🔧 [CORE-AREA] Core area check complete");
4484        Ok(())
4485    }
4486
4487    /// Save the connectome as a genome JSON
4488    ///
4489    /// **DEPRECATED**: This method produces incomplete hierarchical format v2.1 without morphologies/physiology.
4490    /// Use `GenomeService::save_genome()` instead, which produces complete flat format v3.0.
4491    ///
4492    /// This method is kept only for legacy tests. Production code MUST use GenomeService.
4493    ///
4494    /// # Arguments
4495    ///
4496    /// * `genome_id` - Optional custom genome ID (generates timestamp-based ID if None)
4497    /// * `genome_title` - Optional custom genome title
4498    ///
4499    /// # Returns
4500    ///
4501    /// JSON string representation of the genome (hierarchical v2.1, incomplete)
4502    ///
4503    #[deprecated(
4504        note = "Use GenomeService::save_genome() instead. This produces incomplete v2.1 format without morphologies/physiology."
4505    )]
4506    #[allow(deprecated)]
4507    pub fn save_genome_to_json(
4508        &self,
4509        genome_id: Option<String>,
4510        genome_title: Option<String>,
4511    ) -> BduResult<String> {
4512        // Build parent map from brain region hierarchy
4513        let mut brain_regions_with_parents = std::collections::HashMap::new();
4514
4515        for region_id in self.brain_regions.get_all_region_ids() {
4516            if let Some(region) = self.brain_regions.get_region(region_id) {
4517                let parent_id = self
4518                    .brain_regions
4519                    .get_parent(region_id)
4520                    .map(|s| s.to_string());
4521                brain_regions_with_parents
4522                    .insert(region_id.to_string(), (region.clone(), parent_id));
4523            }
4524        }
4525
4526        // Generate and return JSON
4527        Ok(feagi_evolutionary::GenomeSaver::save_to_json(
4528            &self.cortical_areas,
4529            &brain_regions_with_parents,
4530            genome_id,
4531            genome_title,
4532        )?)
4533    }
4534
4535    // Load genome from file and develop brain
4536    //
4537    // This was a high-level convenience method that:
4538    // 1. Loads genome from JSON file
4539    // 2. Prepares for new genome (clears existing state)
4540    // 3. Runs neuroembryogenesis to develop the brain
4541    //
4542    // # Arguments
4543    //
4544    // * `genome_path` - Path to genome JSON file
4545    //
4546    // # Returns
4547    //
4548    // Development progress information
4549    //
4550    // NOTE: load_from_genome_file() and load_from_genome() have been REMOVED.
4551    // All genome loading must now go through GenomeService::load_genome() which:
4552    // - Stores RuntimeGenome for persistence
4553    // - Updates genome metadata
4554    // - Provides async/await support
4555    // - Includes timeout protection
4556    // - Ensures core cortical areas exist
4557    //
4558    // See: feagi-services/src/impls/genome_service_impl.rs::load_genome()
4559
4560    /// Prepare for loading a new genome
4561    ///
4562    /// Clears all existing cortical areas, brain regions, and resets state.
4563    /// This is typically called before loading a new genome.
4564    ///
4565    pub fn prepare_for_new_genome(&mut self) -> BduResult<()> {
4566        info!(target: "feagi-bdu","Preparing for new genome (clearing existing state)");
4567
4568        // Clear cortical areas
4569        self.cortical_areas.clear();
4570        self.cortical_id_to_idx.clear();
4571        self.cortical_idx_to_id.clear();
4572        // CRITICAL: Reserve indices 0 (_death) and 1 (_power)
4573        self.next_cortical_idx = 3;
4574        info!("🔧 [BRAIN-RESET] Cortical mapping cleared, next_cortical_idx reset to 3 (reserves 0=_death, 1=_power, 2=_fatigue)");
4575
4576        // Clear brain regions
4577        self.brain_regions = BrainRegionHierarchy::new();
4578
4579        // CRITICAL: NPU is NOT reset here. Until NPU has a reset() and we call it:
4580        // - ConnectomeManager gets new cortical_idx_to_id from the new genome.
4581        // - Burst loop cache is refreshed from ConnectomeManager (refresh_burst_runner_cache).
4582        // - NPU still holds neurons from the OLD genome with OLD cortical_idx.
4583        // - Fire queue (area_id, x, y, z) uses old indices; we label with NEW cortical_id.
4584        // Result: wrong area's coordinates sent under another area's id -> BV shows
4585        // out-of-region / wrong-scale activations. Restarting feagi-rs fixes it (full sync).
4586        // Fix: implement NPU reset (clear neuron storage + area_id_to_name), then:
4587        //   if let Some(ref npu) = self.npu { npu.lock().map_err(...)?.reset(); }
4588
4589        info!(target: "feagi-bdu","✅ Connectome cleared and ready for new genome");
4590        Ok(())
4591    }
4592
4593    /// Calculate and resize memory for a genome
4594    ///
4595    /// Analyzes the genome to determine memory requirements and
4596    /// prepares the NPU for the expected neuron/synapse counts.
4597    ///
4598    /// # Arguments
4599    ///
4600    /// * `genome` - Genome to analyze for memory requirements
4601    ///
4602    pub fn resize_for_genome(
4603        &mut self,
4604        genome: &feagi_evolutionary::RuntimeGenome,
4605    ) -> BduResult<()> {
4606        // Store morphologies from genome
4607        self.morphology_registry = genome.morphologies.clone();
4608        info!(target: "feagi-bdu", "Stored {} morphologies from genome", self.morphology_registry.count());
4609
4610        // Calculate required capacity from genome stats
4611        let required_neurons = genome.stats.innate_neuron_count;
4612        let required_synapses = genome.stats.innate_synapse_count;
4613
4614        info!(target: "feagi-bdu",
4615            "Genome requires: {} neurons, {} synapses",
4616            required_neurons,
4617            required_synapses
4618        );
4619
4620        // Calculate total voxels from all cortical areas
4621        let mut total_voxels = 0;
4622        for area in genome.cortical_areas.values() {
4623            total_voxels += area.dimensions.width * area.dimensions.height * area.dimensions.depth;
4624        }
4625
4626        info!(target: "feagi-bdu",
4627            "Genome has {} cortical areas with {} total voxels",
4628            genome.cortical_areas.len(),
4629            total_voxels
4630        );
4631
4632        // TODO: Resize NPU if needed
4633        // For now, we assume NPU has sufficient capacity
4634        // In the future, we may want to dynamically resize the NPU based on genome requirements
4635
4636        Ok(())
4637    }
4638
4639    // ========================================================================
4640    // SYNAPSE OPERATIONS
4641    // ========================================================================
4642
4643    /// Create a synapse between two neurons
4644    ///
4645    /// # Arguments
4646    ///
4647    /// * `source_neuron_id` - Source neuron ID
4648    /// * `target_neuron_id` - Target neuron ID
4649    /// * `weight` - Synapse weight (0-255)
4650    /// * `psp` - Synapse PSP (0-255)
4651    /// * `synapse_type` - Synapse type (0=excitatory, 1=inhibitory)
4652    ///
4653    /// # Returns
4654    ///
4655    /// `Ok(())` if synapse created successfully
4656    ///
4657    pub fn create_synapse(
4658        &mut self,
4659        source_neuron_id: u64,
4660        target_neuron_id: u64,
4661        weight: u8,
4662        psp: u8,
4663        synapse_type: u8,
4664    ) -> BduResult<()> {
4665        // Get NPU
4666        let npu = self
4667            .npu
4668            .as_ref()
4669            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4670
4671        let mut npu_lock = npu
4672            .lock()
4673            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4674
4675        // Verify both neurons exist
4676        let source_exists = (source_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
4677        let target_exists = (target_neuron_id as u32) < npu_lock.get_neuron_count() as u32;
4678
4679        if !source_exists {
4680            return Err(BduError::InvalidNeuron(format!(
4681                "Source neuron {} not found",
4682                source_neuron_id
4683            )));
4684        }
4685        if !target_exists {
4686            return Err(BduError::InvalidNeuron(format!(
4687                "Target neuron {} not found",
4688                target_neuron_id
4689            )));
4690        }
4691
4692        // Create synapse via NPU
4693        let syn_type = if synapse_type == 0 {
4694            feagi_npu_neural::synapse::SynapseType::Excitatory
4695        } else {
4696            feagi_npu_neural::synapse::SynapseType::Inhibitory
4697        };
4698
4699        let synapse_idx = npu_lock
4700            .add_synapse(
4701                NeuronId(source_neuron_id as u32),
4702                NeuronId(target_neuron_id as u32),
4703                feagi_npu_neural::types::SynapticWeight(weight),
4704                feagi_npu_neural::types::SynapticPsp(psp),
4705                syn_type,
4706            )
4707            .map_err(|e| BduError::Internal(format!("Failed to create synapse: {}", e)))?;
4708
4709        debug!(target: "feagi-bdu", "Created synapse: {} -> {} (weight: {}, psp: {}, type: {}, idx: {})",
4710            source_neuron_id, target_neuron_id, weight, psp, synapse_type, synapse_idx);
4711
4712        let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
4713        let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
4714        let source_cortical_id = self.cortical_idx_to_id.get(&source_cortical_idx).cloned();
4715        let target_cortical_id = self.cortical_idx_to_id.get(&target_cortical_idx).cloned();
4716
4717        if let Some(state_manager) = StateManager::instance().try_read() {
4718            let core_state = state_manager.get_core_state();
4719            core_state.add_synapse_count(1);
4720            if let Some(cortical_id) = source_cortical_id {
4721                state_manager.add_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
4722            }
4723            if let Some(cortical_id) = target_cortical_id {
4724                state_manager.add_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
4725            }
4726        }
4727
4728        // Trigger fatigue index recalculation after synapse creation
4729        // NOTE: Disabled during genome loading to prevent blocking
4730        // let _ = self.update_fatigue_index();
4731
4732        Ok(())
4733    }
4734
4735    /// Synchronize cortical area flags with NPU
4736    /// This should be called after adding/updating cortical areas
4737    fn sync_cortical_area_flags_to_npu(&mut self) -> BduResult<()> {
4738        if let Some(ref npu) = self.npu {
4739            if let Ok(mut npu_lock) = npu.lock() {
4740                // Build psp_uniform_distribution flags map
4741                let mut psp_uniform_flags = ahash::AHashMap::new();
4742                let mut mp_driven_psp_flags = ahash::AHashMap::new();
4743
4744                for (cortical_id, area) in &self.cortical_areas {
4745                    // Get psp_uniform_distribution flag (default to false)
4746                    let psp_uniform = area
4747                        .get_property("psp_uniform_distribution")
4748                        .and_then(|v| v.as_bool())
4749                        .unwrap_or(false);
4750                    psp_uniform_flags.insert(*cortical_id, psp_uniform);
4751
4752                    // Get mp_driven_psp flag (default to false)
4753                    let mp_driven_psp = area
4754                        .get_property("mp_driven_psp")
4755                        .and_then(|v| v.as_bool())
4756                        .unwrap_or(false);
4757                    mp_driven_psp_flags.insert(*cortical_id, mp_driven_psp);
4758                }
4759
4760                // Update NPU with flags
4761                npu_lock.set_psp_uniform_distribution_flags(psp_uniform_flags);
4762                npu_lock.set_mp_driven_psp_flags(mp_driven_psp_flags);
4763
4764                trace!(
4765                    target: "feagi-bdu",
4766                    "Synchronized cortical area flags to NPU ({} areas)",
4767                    self.cortical_areas.len()
4768                );
4769            }
4770        }
4771
4772        Ok(())
4773    }
4774
4775    /// Get synapse information between two neurons
4776    ///
4777    /// # Arguments
4778    ///
4779    /// * `source_neuron_id` - Source neuron ID
4780    /// * `target_neuron_id` - Target neuron ID
4781    ///
4782    /// # Returns
4783    ///
4784    /// `Some((weight, psp, type))` if synapse exists, `None` otherwise
4785    ///
4786    pub fn get_synapse(
4787        &self,
4788        source_neuron_id: u64,
4789        target_neuron_id: u64,
4790    ) -> Option<(u8, u8, u8)> {
4791        // Get NPU
4792        let npu = self.npu.as_ref()?;
4793        let npu_lock = npu.lock().ok()?;
4794
4795        // Use get_incoming_synapses and filter by source
4796        // (This does O(n) scan of synapse_array, but works even when propagation engine isn't updated)
4797        let incoming = npu_lock.get_incoming_synapses(target_neuron_id as u32);
4798
4799        // Find the synapse from our specific source
4800        for (source_id, weight, psp, synapse_type) in incoming {
4801            if source_id == source_neuron_id as u32 {
4802                return Some((weight, psp, synapse_type));
4803            }
4804        }
4805
4806        None
4807    }
4808
4809    /// Update the weight of an existing synapse
4810    ///
4811    /// # Arguments
4812    ///
4813    /// * `source_neuron_id` - Source neuron ID
4814    /// * `target_neuron_id` - Target neuron ID
4815    /// * `new_weight` - New synapse weight (0-255)
4816    ///
4817    /// # Returns
4818    ///
4819    /// `Ok(())` if synapse updated, `Err` if synapse not found
4820    ///
4821    pub fn update_synapse_weight(
4822        &mut self,
4823        source_neuron_id: u64,
4824        target_neuron_id: u64,
4825        new_weight: u8,
4826    ) -> BduResult<()> {
4827        // Get NPU
4828        let npu = self
4829            .npu
4830            .as_ref()
4831            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4832
4833        let mut npu_lock = npu
4834            .lock()
4835            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4836
4837        // Update synapse weight via NPU
4838        let updated = npu_lock.update_synapse_weight(
4839            NeuronId(source_neuron_id as u32),
4840            NeuronId(target_neuron_id as u32),
4841            feagi_npu_neural::types::SynapticWeight(new_weight),
4842        );
4843
4844        if updated {
4845            debug!(target: "feagi-bdu","Updated synapse weight: {} -> {} = {}", source_neuron_id, target_neuron_id, new_weight);
4846            Ok(())
4847        } else {
4848            Err(BduError::InvalidSynapse(format!(
4849                "Synapse {} -> {} not found",
4850                source_neuron_id, target_neuron_id
4851            )))
4852        }
4853    }
4854
4855    /// Remove a synapse between two neurons
4856    ///
4857    /// # Arguments
4858    ///
4859    /// * `source_neuron_id` - Source neuron ID
4860    /// * `target_neuron_id` - Target neuron ID
4861    ///
4862    /// # Returns
4863    ///
4864    /// `Ok(true)` if synapse removed, `Ok(false)` if synapse didn't exist
4865    ///
4866    pub fn remove_synapse(
4867        &mut self,
4868        source_neuron_id: u64,
4869        target_neuron_id: u64,
4870    ) -> BduResult<bool> {
4871        // Get NPU
4872        let npu = self
4873            .npu
4874            .as_ref()
4875            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4876
4877        let mut npu_lock = npu
4878            .lock()
4879            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4880
4881        let source_cortical_idx = npu_lock.get_neuron_cortical_area(source_neuron_id as u32);
4882        let target_cortical_idx = npu_lock.get_neuron_cortical_area(target_neuron_id as u32);
4883        let source_cortical_id = self.cortical_idx_to_id.get(&source_cortical_idx).cloned();
4884        let target_cortical_id = self.cortical_idx_to_id.get(&target_cortical_idx).cloned();
4885
4886        // Remove synapse via NPU
4887        let removed = npu_lock.remove_synapse(
4888            NeuronId(source_neuron_id as u32),
4889            NeuronId(target_neuron_id as u32),
4890        );
4891
4892        if removed {
4893            debug!(target: "feagi-bdu","Removed synapse: {} -> {}", source_neuron_id, target_neuron_id);
4894
4895            // CRITICAL: Update StateManager synapse count (for health_check endpoint)
4896            if let Some(state_manager) = StateManager::instance().try_read() {
4897                let core_state = state_manager.get_core_state();
4898                core_state.subtract_synapse_count(1);
4899                if let Some(cortical_id) = source_cortical_id {
4900                    state_manager
4901                        .subtract_cortical_area_outgoing_synapses(&cortical_id.as_base_64(), 1);
4902                }
4903                if let Some(cortical_id) = target_cortical_id {
4904                    state_manager
4905                        .subtract_cortical_area_incoming_synapses(&cortical_id.as_base_64(), 1);
4906                }
4907            }
4908        }
4909
4910        Ok(removed)
4911    }
4912
4913    // ========================================================================
4914    // BATCH OPERATIONS
4915    // ========================================================================
4916
4917    /// Batch create multiple neurons at once (SIMD-optimized)
4918    ///
4919    /// This is significantly faster than calling `add_neuron()` in a loop
4920    ///
4921    /// # Arguments
4922    ///
4923    /// * `cortical_id` - Target cortical area
4924    /// * `neurons` - Vector of neuron parameters (x, y, z, firing_threshold, leak, resting_potential, etc.)
4925    ///
4926    /// # Returns
4927    ///
4928    /// Vector of created neuron IDs
4929    ///
4930    pub fn batch_create_neurons(
4931        &mut self,
4932        cortical_id: &CorticalID,
4933        neurons: Vec<NeuronData>,
4934    ) -> BduResult<Vec<u64>> {
4935        // Get NPU
4936        let npu = self
4937            .npu
4938            .as_ref()
4939            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
4940
4941        let mut npu_lock = npu
4942            .lock()
4943            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
4944
4945        // Get cortical area to verify it exists and get its index
4946        let area = self.get_cortical_area(cortical_id).ok_or_else(|| {
4947            BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
4948        })?;
4949        let cortical_idx = area.cortical_idx;
4950
4951        let count = neurons.len();
4952
4953        // Extract parameters into separate vectors for batch operation
4954        let mut x_coords = Vec::with_capacity(count);
4955        let mut y_coords = Vec::with_capacity(count);
4956        let mut z_coords = Vec::with_capacity(count);
4957        let mut firing_thresholds = Vec::with_capacity(count);
4958        let mut threshold_limits = Vec::with_capacity(count);
4959        let mut leak_coeffs = Vec::with_capacity(count);
4960        let mut resting_potentials = Vec::with_capacity(count);
4961        let mut neuron_types = Vec::with_capacity(count);
4962        let mut refractory_periods = Vec::with_capacity(count);
4963        let mut excitabilities = Vec::with_capacity(count);
4964        let mut consec_fire_limits = Vec::with_capacity(count);
4965        let mut snooze_lengths = Vec::with_capacity(count);
4966        let mut mp_accums = Vec::with_capacity(count);
4967        let mut cortical_areas = Vec::with_capacity(count);
4968
4969        for (
4970            x,
4971            y,
4972            z,
4973            threshold,
4974            threshold_limit,
4975            leak,
4976            resting,
4977            ntype,
4978            refract,
4979            excit,
4980            consec_limit,
4981            snooze,
4982            mp_accum,
4983        ) in neurons
4984        {
4985            x_coords.push(x);
4986            y_coords.push(y);
4987            z_coords.push(z);
4988            firing_thresholds.push(threshold);
4989            threshold_limits.push(threshold_limit);
4990            leak_coeffs.push(leak);
4991            resting_potentials.push(resting);
4992            neuron_types.push(ntype);
4993            refractory_periods.push(refract);
4994            excitabilities.push(excit);
4995            consec_fire_limits.push(consec_limit);
4996            snooze_lengths.push(snooze);
4997            mp_accums.push(mp_accum);
4998            cortical_areas.push(cortical_idx);
4999        }
5000
5001        // Get the current neuron count - this will be the first ID of our batch
5002        let first_neuron_id = npu_lock.get_neuron_count() as u32;
5003
5004        // Call NPU batch creation (SIMD-optimized)
5005        // Signature: (thresholds, threshold_limits, leak_coeffs, resting_pots, neuron_types, refract, excit, consec_limits, snooze, mp_accums, cortical_areas, x, y, z)
5006        // Convert f32 vectors to T
5007        // DynamicNPU will handle f32 inputs and convert internally based on its precision
5008        let firing_thresholds_t = firing_thresholds;
5009        let threshold_limits_t = threshold_limits;
5010        let resting_potentials_t = resting_potentials;
5011        let (neurons_created, _indices) = npu_lock.add_neurons_batch(
5012            firing_thresholds_t,
5013            threshold_limits_t,
5014            leak_coeffs,
5015            resting_potentials_t,
5016            neuron_types,
5017            refractory_periods,
5018            excitabilities,
5019            consec_fire_limits,
5020            snooze_lengths,
5021            mp_accums,
5022            cortical_areas,
5023            x_coords,
5024            y_coords,
5025            z_coords,
5026        );
5027
5028        // Generate neuron IDs (they are sequential starting from first_neuron_id)
5029        let mut neuron_ids = Vec::with_capacity(count);
5030        for i in 0..neurons_created {
5031            neuron_ids.push((first_neuron_id + i) as u64);
5032        }
5033
5034        info!(target: "feagi-bdu","Batch created {} neurons in cortical area {}", count, cortical_id);
5035
5036        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
5037        if let Some(state_manager) = StateManager::instance().try_read() {
5038            let core_state = state_manager.get_core_state();
5039            core_state.add_neuron_count(neurons_created);
5040            core_state.add_regular_neuron_count(neurons_created);
5041            state_manager.add_cortical_area_neuron_count(&cortical_id.as_base_64(), count);
5042        }
5043
5044        // Best-effort: keep per-area cache in sync for lock-free reads.
5045        {
5046            let mut cache = self.cached_neuron_counts_per_area.write();
5047            cache
5048                .entry(*cortical_id)
5049                .or_insert_with(|| AtomicUsize::new(0))
5050                .fetch_add(count, Ordering::Relaxed);
5051        }
5052
5053        Ok(neuron_ids)
5054    }
5055
5056    /// Delete multiple neurons at once (batch operation)
5057    ///
5058    /// # Arguments
5059    ///
5060    /// * `neuron_ids` - Vector of neuron IDs to delete
5061    ///
5062    /// # Returns
5063    ///
5064    /// Number of neurons actually deleted
5065    ///
5066    pub fn delete_neurons_batch(&mut self, neuron_ids: Vec<u64>) -> BduResult<usize> {
5067        // Get NPU
5068        let npu = self
5069            .npu
5070            .as_ref()
5071            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5072
5073        let mut npu_lock = npu
5074            .lock()
5075            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5076
5077        let mut deleted_count = 0;
5078        let mut per_area_deleted: std::collections::HashMap<String, usize> =
5079            std::collections::HashMap::new();
5080
5081        // Delete each neuron
5082        // Note: Could be optimized with a batch delete method in NPU if needed
5083        for neuron_id in neuron_ids {
5084            let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
5085            let cortical_id = self.cortical_idx_to_id.get(&cortical_idx).cloned();
5086
5087            if npu_lock.delete_neuron(neuron_id as u32) {
5088                deleted_count += 1;
5089                if let Some(cortical_id) = cortical_id {
5090                    let key = cortical_id.as_base_64();
5091                    *per_area_deleted.entry(key).or_insert(0) += 1;
5092                }
5093            }
5094        }
5095
5096        info!(target: "feagi-bdu","Batch deleted {} neurons", deleted_count);
5097
5098        // CRITICAL: Update StateManager neuron count (for health_check endpoint)
5099        if deleted_count > 0 {
5100            if let Some(state_manager) = StateManager::instance().try_read() {
5101                let core_state = state_manager.get_core_state();
5102                core_state.subtract_neuron_count(deleted_count as u32);
5103                core_state.subtract_regular_neuron_count(deleted_count as u32);
5104                for (cortical_id, count) in per_area_deleted {
5105                    state_manager.subtract_cortical_area_neuron_count(&cortical_id, count);
5106                }
5107            }
5108        }
5109
5110        // Trigger fatigue index recalculation after batch neuron deletion
5111        // NOTE: Disabled during genome loading to prevent blocking
5112        // if deleted_count > 0 {
5113        //     let _ = self.update_fatigue_index();
5114        // }
5115
5116        Ok(deleted_count)
5117    }
5118
5119    // ========================================================================
5120    // NEURON UPDATE OPERATIONS
5121    // ========================================================================
5122
5123    /// Update properties of an existing neuron
5124    ///
5125    /// # Arguments
5126    ///
5127    /// * `neuron_id` - Target neuron ID
5128    /// * `firing_threshold` - Optional new firing threshold
5129    /// * `leak_coefficient` - Optional new leak coefficient
5130    /// * `resting_potential` - Optional new resting potential
5131    /// * `excitability` - Optional new excitability
5132    ///
5133    /// # Returns
5134    ///
5135    /// `Ok(())` if neuron updated successfully
5136    ///
5137    pub fn update_neuron_properties(
5138        &mut self,
5139        neuron_id: u64,
5140        firing_threshold: Option<f32>,
5141        leak_coefficient: Option<f32>,
5142        resting_potential: Option<f32>,
5143        excitability: Option<f32>,
5144    ) -> BduResult<()> {
5145        // Get NPU
5146        let npu = self
5147            .npu
5148            .as_ref()
5149            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5150
5151        let mut npu_lock = npu
5152            .lock()
5153            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5154
5155        let neuron_id_u32 = neuron_id as u32;
5156
5157        // Verify neuron exists by trying to update at least one property
5158        let mut updated = false;
5159
5160        // Update properties if provided
5161        if let Some(threshold) = firing_threshold {
5162            if npu_lock.update_neuron_threshold(neuron_id_u32, threshold) {
5163                updated = true;
5164                debug!(target: "feagi-bdu","Updated neuron {} firing_threshold = {}", neuron_id, threshold);
5165            } else if !updated {
5166                return Err(BduError::InvalidNeuron(format!(
5167                    "Neuron {} not found",
5168                    neuron_id
5169                )));
5170            }
5171        }
5172
5173        if let Some(leak) = leak_coefficient {
5174            if npu_lock.update_neuron_leak(neuron_id_u32, leak) {
5175                updated = true;
5176                debug!(target: "feagi-bdu","Updated neuron {} leak_coefficient = {}", neuron_id, leak);
5177            } else if !updated {
5178                return Err(BduError::InvalidNeuron(format!(
5179                    "Neuron {} not found",
5180                    neuron_id
5181                )));
5182            }
5183        }
5184
5185        if let Some(resting) = resting_potential {
5186            if npu_lock.update_neuron_resting_potential(neuron_id_u32, resting) {
5187                updated = true;
5188                debug!(target: "feagi-bdu","Updated neuron {} resting_potential = {}", neuron_id, resting);
5189            } else if !updated {
5190                return Err(BduError::InvalidNeuron(format!(
5191                    "Neuron {} not found",
5192                    neuron_id
5193                )));
5194            }
5195        }
5196
5197        if let Some(excit) = excitability {
5198            if npu_lock.update_neuron_excitability(neuron_id_u32, excit) {
5199                updated = true;
5200                debug!(target: "feagi-bdu","Updated neuron {} excitability = {}", neuron_id, excit);
5201            } else if !updated {
5202                return Err(BduError::InvalidNeuron(format!(
5203                    "Neuron {} not found",
5204                    neuron_id
5205                )));
5206            }
5207        }
5208
5209        if !updated {
5210            return Err(BduError::Internal(
5211                "No properties provided for update".to_string(),
5212            ));
5213        }
5214
5215        info!(target: "feagi-bdu","Updated properties for neuron {}", neuron_id);
5216
5217        Ok(())
5218    }
5219
5220    /// Update the firing threshold of a specific neuron
5221    ///
5222    /// # Arguments
5223    ///
5224    /// * `neuron_id` - Target neuron ID
5225    /// * `new_threshold` - New firing threshold value
5226    ///
5227    /// # Returns
5228    ///
5229    /// `Ok(())` if threshold updated successfully
5230    ///
5231    pub fn set_neuron_firing_threshold(
5232        &mut self,
5233        neuron_id: u64,
5234        new_threshold: f32,
5235    ) -> BduResult<()> {
5236        // Get NPU
5237        let npu = self
5238            .npu
5239            .as_ref()
5240            .ok_or_else(|| BduError::Internal("NPU not connected".to_string()))?;
5241
5242        let mut npu_lock = npu
5243            .lock()
5244            .map_err(|e| BduError::Internal(format!("Failed to lock NPU: {}", e)))?;
5245
5246        // Update threshold via NPU
5247        if npu_lock.update_neuron_threshold(neuron_id as u32, new_threshold) {
5248            debug!(target: "feagi-bdu","Set neuron {} firing threshold = {}", neuron_id, new_threshold);
5249            Ok(())
5250        } else {
5251            Err(BduError::InvalidNeuron(format!(
5252                "Neuron {} not found",
5253                neuron_id
5254            )))
5255        }
5256    }
5257
5258    // ========================================================================
5259    // AREA MANAGEMENT & QUERIES
5260    // ========================================================================
5261
5262    /// Get cortical area by name (alternative to ID lookup)
5263    ///
5264    /// # Arguments
5265    ///
5266    /// * `name` - Human-readable area name
5267    ///
5268    /// # Returns
5269    ///
5270    /// `Some(CorticalArea)` if found, `None` otherwise
5271    ///
5272    pub fn get_cortical_area_by_name(&self, name: &str) -> Option<CorticalArea> {
5273        self.cortical_areas
5274            .values()
5275            .find(|area| area.name == name)
5276            .cloned()
5277    }
5278
5279    /// Resize a cortical area (changes dimensions, may require neuron reallocation)
5280    ///
5281    /// # Arguments
5282    ///
5283    /// * `cortical_id` - Target cortical area ID
5284    /// * `new_dimensions` - New dimensions (width, height, depth)
5285    ///
5286    /// # Returns
5287    ///
5288    /// `Ok(())` if resized successfully
5289    ///
5290    /// # Note
5291    ///
5292    /// This does NOT automatically create/delete neurons. It only updates metadata.
5293    /// Caller must handle neuron population separately.
5294    ///
5295    pub fn resize_cortical_area(
5296        &mut self,
5297        cortical_id: &CorticalID,
5298        new_dimensions: CorticalAreaDimensions,
5299    ) -> BduResult<()> {
5300        // Validate dimensions
5301        if new_dimensions.width == 0 || new_dimensions.height == 0 || new_dimensions.depth == 0 {
5302            return Err(BduError::InvalidArea(format!(
5303                "Invalid dimensions: {:?} (all must be > 0)",
5304                new_dimensions
5305            )));
5306        }
5307
5308        // Get and update area
5309        let area = self.cortical_areas.get_mut(cortical_id).ok_or_else(|| {
5310            BduError::InvalidArea(format!("Cortical area {} not found", cortical_id))
5311        })?;
5312
5313        let old_dimensions = area.dimensions;
5314        area.dimensions = new_dimensions;
5315
5316        // Note: Visualization voxel granularity is user-driven, not recalculated on resize
5317        // If user had set a custom value, it remains; otherwise defaults to 1x1x1
5318
5319        info!(target: "feagi-bdu",
5320            "Resized cortical area {} from {:?} to {:?}",
5321            cortical_id,
5322            old_dimensions,
5323            new_dimensions
5324        );
5325
5326        self.refresh_cortical_area_hashes(false, true);
5327
5328        Ok(())
5329    }
5330
5331    /// Get all cortical areas in a brain region
5332    ///
5333    /// # Arguments
5334    ///
5335    /// * `region_id` - Brain region ID
5336    ///
5337    /// # Returns
5338    ///
5339    /// Vector of cortical area IDs in the region
5340    ///
5341    pub fn get_areas_in_region(&self, region_id: &str) -> BduResult<Vec<String>> {
5342        let region = self.brain_regions.get_region(region_id).ok_or_else(|| {
5343            BduError::InvalidArea(format!("Brain region {} not found", region_id))
5344        })?;
5345
5346        // Convert CorticalID to base64 strings
5347        Ok(region
5348            .cortical_areas
5349            .iter()
5350            .map(|id| id.as_base_64())
5351            .collect())
5352    }
5353
5354    /// Update brain region properties
5355    ///
5356    /// # Arguments
5357    ///
5358    /// * `region_id` - Target region ID
5359    /// * `new_name` - Optional new name
5360    /// * `new_description` - Optional new description
5361    ///
5362    /// # Returns
5363    ///
5364    /// `Ok(())` if updated successfully
5365    ///
5366    pub fn update_brain_region(
5367        &mut self,
5368        region_id: &str,
5369        new_name: Option<String>,
5370        new_description: Option<String>,
5371    ) -> BduResult<()> {
5372        let region = self
5373            .brain_regions
5374            .get_region_mut(region_id)
5375            .ok_or_else(|| {
5376                BduError::InvalidArea(format!("Brain region {} not found", region_id))
5377            })?;
5378
5379        if let Some(name) = new_name {
5380            region.name = name;
5381            debug!(target: "feagi-bdu","Updated brain region {} name", region_id);
5382        }
5383
5384        if let Some(desc) = new_description {
5385            // BrainRegion doesn't have a description field in the struct, so we'll store it in properties
5386            region
5387                .properties
5388                .insert("description".to_string(), serde_json::json!(desc));
5389            debug!(target: "feagi-bdu","Updated brain region {} description", region_id);
5390        }
5391
5392        info!(target: "feagi-bdu","Updated brain region {}", region_id);
5393
5394        self.refresh_brain_regions_hash();
5395
5396        Ok(())
5397    }
5398
5399    /// Update brain region properties with generic property map
5400    ///
5401    /// Supports updating any brain region property including coordinates, title, description, etc.
5402    ///
5403    /// # Arguments
5404    ///
5405    /// * `region_id` - Target region ID
5406    /// * `properties` - Map of property names to new values
5407    ///
5408    /// # Returns
5409    ///
5410    /// `Ok(())` if updated successfully
5411    ///
5412    pub fn update_brain_region_properties(
5413        &mut self,
5414        region_id: &str,
5415        properties: std::collections::HashMap<String, serde_json::Value>,
5416    ) -> BduResult<()> {
5417        use tracing::{debug, info};
5418
5419        let region = self
5420            .brain_regions
5421            .get_region_mut(region_id)
5422            .ok_or_else(|| {
5423                BduError::InvalidArea(format!("Brain region {} not found", region_id))
5424            })?;
5425
5426        for (key, value) in properties {
5427            match key.as_str() {
5428                "title" | "name" => {
5429                    if let Some(name) = value.as_str() {
5430                        region.name = name.to_string();
5431                        debug!(target: "feagi-bdu", "Updated brain region {} name = {}", region_id, name);
5432                    }
5433                }
5434                "coordinate_3d" | "coordinates_3d" => {
5435                    region
5436                        .properties
5437                        .insert("coordinate_3d".to_string(), value.clone());
5438                    debug!(target: "feagi-bdu", "Updated brain region {} coordinate_3d = {:?}", region_id, value);
5439                }
5440                "coordinate_2d" | "coordinates_2d" => {
5441                    region
5442                        .properties
5443                        .insert("coordinate_2d".to_string(), value.clone());
5444                    debug!(target: "feagi-bdu", "Updated brain region {} coordinate_2d = {:?}", region_id, value);
5445                }
5446                "description" => {
5447                    region
5448                        .properties
5449                        .insert("description".to_string(), value.clone());
5450                    debug!(target: "feagi-bdu", "Updated brain region {} description", region_id);
5451                }
5452                "region_type" => {
5453                    if let Some(type_str) = value.as_str() {
5454                        // Note: RegionType is currently a placeholder (Undefined only)
5455                        // Specific region types will be added in the future
5456                        region.region_type = feagi_structures::genomic::RegionType::Undefined;
5457                        debug!(target: "feagi-bdu", "Updated brain region {} type = {}", region_id, type_str);
5458                    }
5459                }
5460                // Store any other properties in the properties map
5461                _ => {
5462                    region.properties.insert(key.clone(), value.clone());
5463                    debug!(target: "feagi-bdu", "Updated brain region {} property {} = {:?}", region_id, key, value);
5464                }
5465            }
5466        }
5467
5468        info!(target: "feagi-bdu", "Updated brain region {} properties", region_id);
5469
5470        Ok(())
5471    }
5472
5473    // ========================================================================
5474    // NEURON QUERY METHODS (P6)
5475    // ========================================================================
5476
5477    /// Get neuron by 3D coordinates within a cortical area
5478    ///
5479    /// # Arguments
5480    ///
5481    /// * `cortical_id` - Cortical area ID
5482    /// * `x` - X coordinate
5483    /// * `y` - Y coordinate
5484    /// * `z` - Z coordinate
5485    ///
5486    /// # Returns
5487    ///
5488    /// `Some(neuron_id)` if found, `None` otherwise
5489    ///
5490    pub fn get_neuron_by_coordinates(
5491        &self,
5492        cortical_id: &CorticalID,
5493        x: u32,
5494        y: u32,
5495        z: u32,
5496    ) -> Option<u64> {
5497        // Get cortical area to get its index
5498        let area = self.get_cortical_area(cortical_id)?;
5499        let cortical_idx = area.cortical_idx;
5500
5501        // Query NPU via public method
5502        let npu = self.npu.as_ref()?;
5503        let npu_lock = npu.lock().ok()?;
5504
5505        npu_lock
5506            .get_neuron_id_at_coordinate(cortical_idx, x, y, z)
5507            .map(|id| id as u64)
5508    }
5509
5510    /// Get the position (coordinates) of a neuron
5511    ///
5512    /// # Arguments
5513    ///
5514    /// * `neuron_id` - Neuron ID
5515    ///
5516    /// # Returns
5517    ///
5518    /// `Some((x, y, z))` if found, `None` otherwise
5519    ///
5520    pub fn get_neuron_position(&self, neuron_id: u64) -> Option<(u32, u32, u32)> {
5521        let npu = self.npu.as_ref()?;
5522        let npu_lock = npu.lock().ok()?;
5523
5524        // Verify neuron exists and get coordinates
5525        let neuron_count = npu_lock.get_neuron_count();
5526        if (neuron_id as usize) >= neuron_count {
5527            return None;
5528        }
5529
5530        Some(
5531            npu_lock
5532                .get_neuron_coordinates(neuron_id as u32)
5533                .unwrap_or((0, 0, 0)),
5534        )
5535    }
5536
5537    /// Get which cortical area contains a specific neuron
5538    ///
5539    /// # Arguments
5540    ///
5541    /// * `neuron_id` - Neuron ID
5542    ///
5543    /// # Returns
5544    ///
5545    /// `Some(cortical_id)` if found, `None` otherwise
5546    ///
5547    pub fn get_cortical_area_for_neuron(&self, neuron_id: u64) -> Option<CorticalID> {
5548        let npu = self.npu.as_ref()?;
5549        let npu_lock = npu.lock().ok()?;
5550
5551        // Verify neuron exists
5552        let neuron_count = npu_lock.get_neuron_count();
5553        if (neuron_id as usize) >= neuron_count {
5554            return None;
5555        }
5556
5557        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id as u32);
5558
5559        // Look up cortical_id from index
5560        self.cortical_areas
5561            .values()
5562            .find(|area| area.cortical_idx == cortical_idx)
5563            .map(|area| area.cortical_id)
5564    }
5565
5566    /// Get all properties of a neuron
5567    ///
5568    /// # Arguments
5569    ///
5570    /// * `neuron_id` - Neuron ID
5571    ///
5572    /// # Returns
5573    ///
5574    /// `Some(properties)` if found, `None` otherwise
5575    ///
5576    pub fn get_neuron_properties(
5577        &self,
5578        neuron_id: u64,
5579    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
5580        let npu = self.npu.as_ref()?;
5581        let npu_lock = npu.lock().ok()?;
5582
5583        let neuron_id_u32 = neuron_id as u32;
5584        let idx = neuron_id as usize;
5585
5586        // Verify neuron exists
5587        let neuron_count = npu_lock.get_neuron_count();
5588        if idx >= neuron_count {
5589            return None;
5590        }
5591
5592        let mut properties = std::collections::HashMap::new();
5593
5594        // Basic info
5595        properties.insert("neuron_id".to_string(), serde_json::json!(neuron_id));
5596
5597        // Get coordinates
5598        let (x, y, z) = npu_lock.get_neuron_coordinates(neuron_id_u32)?;
5599        properties.insert("x".to_string(), serde_json::json!(x));
5600        properties.insert("y".to_string(), serde_json::json!(y));
5601        properties.insert("z".to_string(), serde_json::json!(z));
5602
5603        // Get cortical area
5604        let cortical_idx = npu_lock.get_neuron_cortical_area(neuron_id_u32);
5605        properties.insert("cortical_area".to_string(), serde_json::json!(cortical_idx));
5606
5607        // Get neuron state (returns: consecutive_fire_count, consecutive_fire_limit, snooze_period, membrane_potential, threshold, refractory_countdown)
5608        if let Some((consec_count, consec_limit, snooze, mp, threshold, refract_countdown)) =
5609            npu_lock.get_neuron_state(NeuronId(neuron_id_u32))
5610        {
5611            properties.insert(
5612                "consecutive_fire_count".to_string(),
5613                serde_json::json!(consec_count),
5614            );
5615            properties.insert(
5616                "consecutive_fire_limit".to_string(),
5617                serde_json::json!(consec_limit),
5618            );
5619            properties.insert("snooze_period".to_string(), serde_json::json!(snooze));
5620            properties.insert("membrane_potential".to_string(), serde_json::json!(mp));
5621            properties.insert("threshold".to_string(), serde_json::json!(threshold));
5622            properties.insert(
5623                "refractory_countdown".to_string(),
5624                serde_json::json!(refract_countdown),
5625            );
5626        }
5627
5628        // Get other properties via get_neuron_property_by_index
5629        if let Some(leak) = npu_lock.get_neuron_property_by_index(idx, "leak_coefficient") {
5630            properties.insert("leak_coefficient".to_string(), serde_json::json!(leak));
5631        }
5632        if let Some(resting) = npu_lock.get_neuron_property_by_index(idx, "resting_potential") {
5633            properties.insert("resting_potential".to_string(), serde_json::json!(resting));
5634        }
5635        if let Some(excit) = npu_lock.get_neuron_property_by_index(idx, "excitability") {
5636            properties.insert("excitability".to_string(), serde_json::json!(excit));
5637        }
5638        if let Some(threshold_limit) = npu_lock.get_neuron_property_by_index(idx, "threshold_limit")
5639        {
5640            properties.insert(
5641                "threshold_limit".to_string(),
5642                serde_json::json!(threshold_limit),
5643            );
5644        }
5645
5646        // Get u16 properties
5647        if let Some(refract_period) =
5648            npu_lock.get_neuron_property_u16_by_index(idx, "refractory_period")
5649        {
5650            properties.insert(
5651                "refractory_period".to_string(),
5652                serde_json::json!(refract_period),
5653            );
5654        }
5655
5656        Some(properties)
5657    }
5658
5659    /// Get a specific property of a neuron
5660    ///
5661    /// # Arguments
5662    ///
5663    /// * `neuron_id` - Neuron ID
5664    /// * `property_name` - Name of the property to retrieve
5665    ///
5666    /// # Returns
5667    ///
5668    /// `Some(value)` if found, `None` otherwise
5669    ///
5670    pub fn get_neuron_property(
5671        &self,
5672        neuron_id: u64,
5673        property_name: &str,
5674    ) -> Option<serde_json::Value> {
5675        self.get_neuron_properties(neuron_id)?
5676            .get(property_name)
5677            .cloned()
5678    }
5679
5680    // ========================================================================
5681    // CORTICAL AREA LIST/QUERY METHODS (P6)
5682    // ========================================================================
5683
5684    /// Get all cortical area IDs
5685    ///
5686    /// # Returns
5687    ///
5688    /// Vector of all cortical area IDs
5689    ///
5690    pub fn get_all_cortical_ids(&self) -> Vec<CorticalID> {
5691        self.cortical_areas.keys().copied().collect()
5692    }
5693
5694    /// Get all cortical area indices
5695    ///
5696    /// # Returns
5697    ///
5698    /// Vector of all cortical area indices
5699    ///
5700    pub fn get_all_cortical_indices(&self) -> Vec<u32> {
5701        self.cortical_areas
5702            .values()
5703            .map(|area| area.cortical_idx)
5704            .collect()
5705    }
5706
5707    /// Get all cortical area names
5708    ///
5709    /// # Returns
5710    ///
5711    /// Vector of all cortical area names
5712    ///
5713    pub fn get_cortical_area_names(&self) -> Vec<String> {
5714        self.cortical_areas
5715            .values()
5716            .map(|area| area.name.clone())
5717            .collect()
5718    }
5719
5720    /// List all input (IPU/sensory) cortical areas
5721    ///
5722    /// # Returns
5723    ///
5724    /// Vector of IPU/sensory area IDs
5725    ///
5726    pub fn list_ipu_areas(&self) -> Vec<CorticalID> {
5727        use crate::models::CorticalAreaExt;
5728        self.cortical_areas
5729            .values()
5730            .filter(|area| area.is_input_area())
5731            .map(|area| area.cortical_id)
5732            .collect()
5733    }
5734
5735    /// List all output (OPU/motor) cortical areas
5736    ///
5737    /// # Returns
5738    ///
5739    /// Vector of OPU/motor area IDs
5740    ///
5741    pub fn list_opu_areas(&self) -> Vec<CorticalID> {
5742        use crate::models::CorticalAreaExt;
5743        self.cortical_areas
5744            .values()
5745            .filter(|area| area.is_output_area())
5746            .map(|area| area.cortical_id)
5747            .collect()
5748    }
5749
5750    /// Get maximum dimensions across all cortical areas
5751    ///
5752    /// # Returns
5753    ///
5754    /// (max_width, max_height, max_depth)
5755    ///
5756    pub fn get_max_cortical_area_dimensions(&self) -> (usize, usize, usize) {
5757        self.cortical_areas
5758            .values()
5759            .fold((0, 0, 0), |(max_w, max_h, max_d), area| {
5760                (
5761                    max_w.max(area.dimensions.width as usize),
5762                    max_h.max(area.dimensions.height as usize),
5763                    max_d.max(area.dimensions.depth as usize),
5764                )
5765            })
5766    }
5767
5768    /// Get all properties of a cortical area as a JSON-serializable map
5769    ///
5770    /// # Arguments
5771    ///
5772    /// * `cortical_id` - Cortical area ID
5773    ///
5774    /// # Returns
5775    ///
5776    /// `Some(properties)` if found, `None` otherwise
5777    ///
5778    pub fn get_cortical_area_properties(
5779        &self,
5780        cortical_id: &CorticalID,
5781    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
5782        let area = self.get_cortical_area(cortical_id)?;
5783
5784        let mut properties = std::collections::HashMap::new();
5785        properties.insert(
5786            "cortical_id".to_string(),
5787            serde_json::json!(area.cortical_id),
5788        );
5789        properties.insert(
5790            "cortical_id_s".to_string(),
5791            serde_json::json!(area.cortical_id.to_string()),
5792        );
5793        properties.insert(
5794            "cortical_idx".to_string(),
5795            serde_json::json!(area.cortical_idx),
5796        );
5797        properties.insert("name".to_string(), serde_json::json!(area.name));
5798        use crate::models::CorticalAreaExt;
5799        properties.insert(
5800            "area_type".to_string(),
5801            serde_json::json!(area.get_cortical_group()),
5802        );
5803        properties.insert(
5804            "dimensions".to_string(),
5805            serde_json::json!({
5806                "width": area.dimensions.width,
5807                "height": area.dimensions.height,
5808                "depth": area.dimensions.depth,
5809            }),
5810        );
5811        properties.insert("position".to_string(), serde_json::json!(area.position));
5812
5813        // Copy all properties from area.properties to the response
5814        for (key, value) in &area.properties {
5815            properties.insert(key.clone(), value.clone());
5816        }
5817
5818        // Add custom properties
5819        properties.extend(area.properties.clone());
5820
5821        Some(properties)
5822    }
5823
5824    /// Get properties of all cortical areas
5825    ///
5826    /// # Returns
5827    ///
5828    /// Vector of property maps for all areas
5829    ///
5830    pub fn get_all_cortical_area_properties(
5831        &self,
5832    ) -> Vec<std::collections::HashMap<String, serde_json::Value>> {
5833        self.cortical_areas
5834            .keys()
5835            .filter_map(|id| self.get_cortical_area_properties(id))
5836            .collect()
5837    }
5838
5839    // ========================================================================
5840    // BRAIN REGION QUERY METHODS (P6)
5841    // ========================================================================
5842
5843    /// Get all brain region IDs
5844    ///
5845    /// # Returns
5846    ///
5847    /// Vector of all brain region IDs
5848    ///
5849    pub fn get_all_brain_region_ids(&self) -> Vec<String> {
5850        self.brain_regions
5851            .get_all_region_ids()
5852            .into_iter()
5853            .cloned()
5854            .collect()
5855    }
5856
5857    /// Get all brain region names
5858    ///
5859    /// # Returns
5860    ///
5861    /// Vector of all brain region names
5862    ///
5863    pub fn get_brain_region_names(&self) -> Vec<String> {
5864        self.brain_regions
5865            .get_all_region_ids()
5866            .iter()
5867            .filter_map(|id| {
5868                self.brain_regions
5869                    .get_region(id)
5870                    .map(|region| region.name.clone())
5871            })
5872            .collect()
5873    }
5874
5875    /// Get properties of a brain region
5876    ///
5877    /// # Arguments
5878    ///
5879    /// * `region_id` - Brain region ID
5880    ///
5881    /// # Returns
5882    ///
5883    /// `Some(properties)` if found, `None` otherwise
5884    ///
5885    pub fn get_brain_region_properties(
5886        &self,
5887        region_id: &str,
5888    ) -> Option<std::collections::HashMap<String, serde_json::Value>> {
5889        let region = self.brain_regions.get_region(region_id)?;
5890
5891        let mut properties = std::collections::HashMap::new();
5892        properties.insert("region_id".to_string(), serde_json::json!(region.region_id));
5893        properties.insert("name".to_string(), serde_json::json!(region.name));
5894        properties.insert(
5895            "region_type".to_string(),
5896            serde_json::json!(format!("{:?}", region.region_type)),
5897        );
5898        properties.insert(
5899            "cortical_areas".to_string(),
5900            serde_json::json!(region.cortical_areas.iter().collect::<Vec<_>>()),
5901        );
5902
5903        // Add custom properties
5904        properties.extend(region.properties.clone());
5905
5906        Some(properties)
5907    }
5908
5909    /// Check if a cortical area exists
5910    ///
5911    /// # Arguments
5912    ///
5913    /// * `cortical_id` - Cortical area ID to check
5914    ///
5915    /// # Returns
5916    ///
5917    /// `true` if area exists, `false` otherwise
5918    ///
5919    pub fn cortical_area_exists(&self, cortical_id: &CorticalID) -> bool {
5920        self.cortical_areas.contains_key(cortical_id)
5921    }
5922
5923    /// Check if a brain region exists
5924    ///
5925    /// # Arguments
5926    ///
5927    /// * `region_id` - Brain region ID to check
5928    ///
5929    /// # Returns
5930    ///
5931    /// `true` if region exists, `false` otherwise
5932    ///
5933    pub fn brain_region_exists(&self, region_id: &str) -> bool {
5934        self.brain_regions.get_region(region_id).is_some()
5935    }
5936
5937    /// Get the total number of brain regions
5938    ///
5939    /// # Returns
5940    ///
5941    /// Number of brain regions
5942    ///
5943    pub fn get_brain_region_count(&self) -> usize {
5944        self.brain_regions.region_count()
5945    }
5946
5947    /// Get neurons by cortical area (alias for get_neurons_in_area for API compatibility)
5948    ///
5949    /// # Arguments
5950    ///
5951    /// * `cortical_id` - Cortical area ID
5952    ///
5953    /// # Returns
5954    ///
5955    /// Vector of neuron IDs in the area
5956    ///
5957    pub fn get_neurons_by_cortical_area(&self, cortical_id: &CorticalID) -> Vec<u64> {
5958        // This is an alias for get_neurons_in_area, which already exists
5959        // Keeping it for Python API compatibility
5960        // Note: The signature says Vec<NeuronId> but implementation returns Vec<u64>
5961        self.get_neurons_in_area(cortical_id)
5962    }
5963}
5964
5965// Manual Debug implementation (RustNPU doesn't implement Debug)
5966impl std::fmt::Debug for ConnectomeManager {
5967    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5968        f.debug_struct("ConnectomeManager")
5969            .field("cortical_areas", &self.cortical_areas.len())
5970            .field("next_cortical_idx", &self.next_cortical_idx)
5971            .field("brain_regions", &self.brain_regions)
5972            .field(
5973                "npu",
5974                &if self.npu.is_some() {
5975                    "Connected"
5976                } else {
5977                    "Not connected"
5978                },
5979            )
5980            .field("initialized", &self.initialized)
5981            .finish()
5982    }
5983}
5984
5985#[cfg(test)]
5986mod tests {
5987    use super::*;
5988    use feagi_structures::genomic::cortical_area::CoreCorticalType;
5989
5990    #[test]
5991    fn test_singleton_instance() {
5992        let instance1 = ConnectomeManager::instance();
5993        let instance2 = ConnectomeManager::instance();
5994
5995        // Both should point to the same instance
5996        assert_eq!(Arc::strong_count(&instance1), Arc::strong_count(&instance2));
5997    }
5998
5999    #[test]
6000    fn test_add_cortical_area() {
6001        ConnectomeManager::reset_for_testing();
6002
6003        let instance = ConnectomeManager::instance();
6004        let mut manager = instance.write();
6005
6006        use feagi_structures::genomic::cortical_area::{
6007            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6008        };
6009        let cortical_id = CorticalID::try_from_bytes(b"cst_add_").unwrap(); // Use unique custom ID
6010        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6011        let area = CorticalArea::new(
6012            cortical_id,
6013            0,
6014            "Visual Input".to_string(),
6015            CorticalAreaDimensions::new(128, 128, 20).unwrap(),
6016            (0, 0, 0).into(),
6017            cortical_type,
6018        )
6019        .unwrap();
6020
6021        let initial_count = manager.get_cortical_area_count();
6022        let _cortical_idx = manager.add_cortical_area(area).unwrap();
6023
6024        assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
6025        assert!(manager.has_cortical_area(&cortical_id));
6026        assert!(manager.is_initialized());
6027    }
6028
6029    #[test]
6030    fn test_cortical_area_lookups() {
6031        ConnectomeManager::reset_for_testing();
6032
6033        let instance = ConnectomeManager::instance();
6034        let mut manager = instance.write();
6035
6036        use feagi_structures::genomic::cortical_area::{
6037            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6038        };
6039        let cortical_id = CorticalID::try_from_bytes(b"cst_look").unwrap(); // Use unique custom ID
6040        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6041        let area = CorticalArea::new(
6042            cortical_id,
6043            0,
6044            "Test Area".to_string(),
6045            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6046            (0, 0, 0).into(),
6047            cortical_type,
6048        )
6049        .unwrap();
6050
6051        let cortical_idx = manager.add_cortical_area(area).unwrap();
6052
6053        // ID -> idx lookup
6054        assert_eq!(manager.get_cortical_idx(&cortical_id), Some(cortical_idx));
6055
6056        // idx -> ID lookup
6057        assert_eq!(manager.get_cortical_id(cortical_idx), Some(&cortical_id));
6058
6059        // Get area
6060        let retrieved_area = manager.get_cortical_area(&cortical_id).unwrap();
6061        assert_eq!(retrieved_area.name, "Test Area");
6062    }
6063
6064    #[test]
6065    fn test_remove_cortical_area() {
6066        ConnectomeManager::reset_for_testing();
6067
6068        let instance = ConnectomeManager::instance();
6069        let mut manager = instance.write();
6070
6071        use feagi_structures::genomic::cortical_area::{
6072            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6073        };
6074        let cortical_id = CoreCorticalType::Power.to_cortical_id();
6075
6076        // Remove area if it already exists from previous tests
6077        if manager.has_cortical_area(&cortical_id) {
6078            manager.remove_cortical_area(&cortical_id).unwrap();
6079        }
6080
6081        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6082        let area = CorticalArea::new(
6083            cortical_id,
6084            0,
6085            "Test".to_string(),
6086            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6087            (0, 0, 0).into(),
6088            cortical_type,
6089        )
6090        .unwrap();
6091
6092        let initial_count = manager.get_cortical_area_count();
6093        manager.add_cortical_area(area).unwrap();
6094        assert_eq!(manager.get_cortical_area_count(), initial_count + 1);
6095
6096        manager.remove_cortical_area(&cortical_id).unwrap();
6097        assert_eq!(manager.get_cortical_area_count(), initial_count);
6098        assert!(!manager.has_cortical_area(&cortical_id));
6099    }
6100
6101    #[test]
6102    fn test_duplicate_area_error() {
6103        ConnectomeManager::reset_for_testing();
6104
6105        let instance = ConnectomeManager::instance();
6106        let mut manager = instance.write();
6107
6108        use feagi_structures::genomic::cortical_area::{
6109            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6110        };
6111        // Use a unique ID only for this test to avoid collisions with other tests (e.g. Power)
6112        // when tests run in parallel; we still test duplicate by adding the same ID twice.
6113        let cortical_id = CorticalID::try_from_bytes(b"cst_dup1").unwrap();
6114        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6115        let area1 = CorticalArea::new(
6116            cortical_id,
6117            0,
6118            "First".to_string(),
6119            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6120            (0, 0, 0).into(),
6121            cortical_type,
6122        )
6123        .unwrap();
6124
6125        let area2 = CorticalArea::new(
6126            cortical_id, // Same ID - duplicate
6127            1,
6128            "Second".to_string(),
6129            CorticalAreaDimensions::new(10, 10, 10).unwrap(),
6130            (0, 0, 0).into(),
6131            cortical_type,
6132        )
6133        .unwrap();
6134
6135        manager.add_cortical_area(area1).unwrap();
6136        let result = manager.add_cortical_area(area2);
6137
6138        assert!(result.is_err());
6139    }
6140
6141    #[test]
6142    fn test_brain_region_management() {
6143        ConnectomeManager::reset_for_testing();
6144
6145        let instance = ConnectomeManager::instance();
6146        let mut manager = instance.write();
6147
6148        let region_id = feagi_structures::genomic::brain_regions::RegionID::new();
6149        let region_id_str = region_id.to_string();
6150        let root = BrainRegion::new(
6151            region_id,
6152            "Root".to_string(),
6153            feagi_structures::genomic::brain_regions::RegionType::Undefined,
6154        )
6155        .unwrap();
6156
6157        let initial_count = manager.get_brain_region_ids().len();
6158        manager.add_brain_region(root, None).unwrap();
6159
6160        assert_eq!(manager.get_brain_region_ids().len(), initial_count + 1);
6161        assert!(manager.get_brain_region(&region_id_str).is_some());
6162    }
6163
6164    #[test]
6165    fn test_synapse_operations() {
6166        use feagi_npu_burst_engine::npu::RustNPU;
6167        use feagi_npu_burst_engine::TracingMutex;
6168        use std::sync::Arc;
6169
6170        // Create NPU and manager for isolated test state
6171        use feagi_npu_burst_engine::backend::CPUBackend;
6172        use feagi_npu_burst_engine::DynamicNPU;
6173        use feagi_npu_runtime::StdRuntime;
6174
6175        let runtime = StdRuntime;
6176        let backend = CPUBackend::new();
6177        let npu_result =
6178            RustNPU::new(runtime, backend, 100, 1000, 10).expect("Failed to create NPU");
6179        let npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu_result), "TestNPU"));
6180        let mut manager = ConnectomeManager::new_for_testing_with_npu(npu.clone());
6181
6182        // First create a cortical area to add neurons to
6183        use feagi_structures::genomic::cortical_area::{
6184            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6185        };
6186        let cortical_id = CorticalID::try_from_bytes(b"cst_syn_").unwrap(); // Use unique custom ID
6187        let cortical_type = CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean);
6188        let area = CorticalArea::new(
6189            cortical_id,
6190            0, // cortical_idx
6191            "Test Area".to_string(),
6192            CorticalAreaDimensions::new(10, 10, 1).unwrap(),
6193            (0, 0, 0).into(), // position
6194            cortical_type,
6195        )
6196        .unwrap();
6197        let cortical_idx = manager.add_cortical_area(area).unwrap();
6198
6199        // Register the cortical area with the NPU using the cortical ID's base64 representation
6200        if let Some(npu_arc) = manager.get_npu() {
6201            if let Ok(mut npu_guard) = npu_arc.try_lock() {
6202                if let DynamicNPU::F32(ref mut npu) = *npu_guard {
6203                    npu.register_cortical_area(cortical_idx, cortical_id.as_base_64());
6204                }
6205            }
6206        }
6207
6208        // Create two neurons
6209        let neuron1_id = manager
6210            .add_neuron(
6211                &cortical_id,
6212                0,
6213                0,
6214                0,     // coordinates
6215                100.0, // firing_threshold
6216                0.0,   // firing_threshold_limit (0 = no limit)
6217                0.1,   // leak_coefficient
6218                -60.0, // resting_potential
6219                0,     // neuron_type
6220                2,     // refractory_period
6221                1.0,   // excitability
6222                5,     // consecutive_fire_limit
6223                10,    // snooze_length
6224                false, // mp_charge_accumulation
6225            )
6226            .unwrap();
6227
6228        let neuron2_id = manager
6229            .add_neuron(
6230                &cortical_id,
6231                1,
6232                0,
6233                0, // coordinates
6234                100.0,
6235                f32::MAX, // firing_threshold_limit (MAX = no limit, SIMD-friendly encoding)
6236                0.1,
6237                -60.0,
6238                0,
6239                2,
6240                1.0,
6241                5,
6242                10,
6243                false,
6244            )
6245            .unwrap();
6246
6247        // Test create_synapse (creation should succeed)
6248        manager
6249            .create_synapse(
6250                neuron1_id, neuron2_id, 128, // weight
6251                64,  // psp
6252                0,   // excitatory
6253            )
6254            .unwrap();
6255
6256        // Note: Synapse retrieval/update/removal tests require full NPU propagation engine initialization
6257        // which is beyond the scope of this unit test. The important part is that create_synapse succeeds.
6258        println!("✅ Synapse creation test passed");
6259    }
6260
6261    #[test]
6262    fn test_apply_cortical_mapping_missing_rules_is_ok() {
6263        // This guards against a regression where deleting a mapping causes a 500 because
6264        // synapse regeneration treats "no mapping rules" as an error.
6265        let mut manager = ConnectomeManager::new_for_testing();
6266
6267        use feagi_structures::genomic::cortical_area::{
6268            CorticalAreaType, IOCorticalAreaConfigurationFlag,
6269        };
6270
6271        let src_id = CorticalID::try_from_bytes(b"map_src_").unwrap();
6272        let dst_id = CorticalID::try_from_bytes(b"map_dst_").unwrap();
6273
6274        let src_area = CorticalArea::new(
6275            src_id,
6276            0,
6277            "src".to_string(),
6278            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6279            (0, 0, 0).into(),
6280            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6281        )
6282        .unwrap();
6283
6284        let dst_area = CorticalArea::new(
6285            dst_id,
6286            1,
6287            "dst".to_string(),
6288            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6289            (0, 0, 0).into(),
6290            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6291        )
6292        .unwrap();
6293
6294        manager.add_cortical_area(src_area).unwrap();
6295        manager.add_cortical_area(dst_area).unwrap();
6296
6297        // No cortical_mapping_dst property set -> should be Ok(0), not an error
6298        let count = manager
6299            .apply_cortical_mapping_for_pair(&src_id, &dst_id)
6300            .unwrap();
6301        assert_eq!(count, 0);
6302
6303        // Now create then delete mapping; missing destination rules should still be Ok(0)
6304        manager
6305            .update_cortical_mapping(
6306                &src_id,
6307                &dst_id,
6308                vec![serde_json::json!({"morphology_id":"m1"})],
6309            )
6310            .unwrap();
6311        manager
6312            .update_cortical_mapping(&src_id, &dst_id, vec![])
6313            .unwrap();
6314
6315        let count2 = manager
6316            .apply_cortical_mapping_for_pair(&src_id, &dst_id)
6317            .unwrap();
6318        assert_eq!(count2, 0);
6319    }
6320
6321    #[test]
6322    fn test_mapping_deletion_prunes_synapses_between_areas() {
6323        use feagi_npu_burst_engine::backend::CPUBackend;
6324        use feagi_npu_burst_engine::RustNPU;
6325        use feagi_npu_burst_engine::TracingMutex;
6326        use feagi_npu_runtime::StdRuntime;
6327        use feagi_structures::genomic::cortical_area::{
6328            CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
6329        };
6330        use std::sync::Arc;
6331
6332        // Create NPU and manager (small capacities for a deterministic unit test)
6333        let runtime = StdRuntime;
6334        let backend = CPUBackend::new();
6335        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6336        let dyn_npu = Arc::new(TracingMutex::new(
6337            feagi_npu_burst_engine::DynamicNPU::F32(npu),
6338            "TestNPU",
6339        ));
6340        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6341
6342        // Create two cortical areas
6343        let src_id = CorticalID::try_from_bytes(b"cst_src_").unwrap();
6344        let dst_id = CorticalID::try_from_bytes(b"cst_dst_").unwrap();
6345
6346        let src_area = CorticalArea::new(
6347            src_id,
6348            0,
6349            "src".to_string(),
6350            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6351            (0, 0, 0).into(),
6352            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6353        )
6354        .unwrap();
6355        let dst_area = CorticalArea::new(
6356            dst_id,
6357            1,
6358            "dst".to_string(),
6359            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6360            (0, 0, 0).into(),
6361            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6362        )
6363        .unwrap();
6364
6365        manager.add_cortical_area(src_area).unwrap();
6366        manager.add_cortical_area(dst_area).unwrap();
6367
6368        // Add a couple neurons to each area
6369        let s0 = manager
6370            .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6371            .unwrap();
6372        let s1 = manager
6373            .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6374            .unwrap();
6375        let t0 = manager
6376            .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6377            .unwrap();
6378        let t1 = manager
6379            .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6380            .unwrap();
6381
6382        // Create synapses that represent an established mapping between the two areas
6383        manager.create_synapse(s0, t0, 128, 200, 0).unwrap();
6384        manager.create_synapse(s1, t1, 128, 200, 0).unwrap();
6385
6386        // Build index once before pruning
6387        {
6388            let mut npu = dyn_npu.lock().unwrap();
6389            npu.rebuild_synapse_index();
6390            assert_eq!(npu.get_synapse_count(), 2);
6391        }
6392
6393        // Simulate mapping deletion and regeneration: should prune synapses and not re-add any
6394        manager
6395            .update_cortical_mapping(&src_id, &dst_id, vec![])
6396            .unwrap();
6397        let created = manager
6398            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6399            .unwrap();
6400        assert_eq!(created, 0);
6401
6402        // Verify synapses are gone (invalidated) and no outgoing synapses remain from the sources
6403        {
6404            let mut npu = dyn_npu.lock().unwrap();
6405            // Pruning invalidates synapses; rebuild the index so counts/outgoing queries reflect the current state.
6406            npu.rebuild_synapse_index();
6407            assert_eq!(npu.get_synapse_count(), 0);
6408            assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
6409            assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
6410        }
6411    }
6412
6413    #[test]
6414    fn test_mapping_update_prunes_synapses_between_areas() {
6415        use feagi_npu_burst_engine::backend::CPUBackend;
6416        use feagi_npu_burst_engine::RustNPU;
6417        use feagi_npu_burst_engine::TracingMutex;
6418        use feagi_npu_runtime::StdRuntime;
6419        use feagi_structures::genomic::cortical_area::{
6420            CorticalAreaDimensions, CorticalAreaType, IOCorticalAreaConfigurationFlag,
6421        };
6422        use std::sync::Arc;
6423
6424        // Create NPU and manager (small capacities for a deterministic unit test)
6425        let runtime = StdRuntime;
6426        let backend = CPUBackend::new();
6427        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6428        let dyn_npu = Arc::new(TracingMutex::new(
6429            feagi_npu_burst_engine::DynamicNPU::F32(npu),
6430            "TestNPU",
6431        ));
6432        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6433
6434        // Seed core morphologies so mapping regeneration can resolve function morphologies (e.g. "episodic_memory").
6435        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6436
6437        // Create two cortical areas
6438        // Use valid custom cortical IDs (the `cst...` namespace).
6439        let src_id = CorticalID::try_from_bytes(b"cstupds1").unwrap();
6440        let dst_id = CorticalID::try_from_bytes(b"cstupdt1").unwrap();
6441
6442        let src_area = CorticalArea::new(
6443            src_id,
6444            0,
6445            "src".to_string(),
6446            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6447            (0, 0, 0).into(),
6448            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6449        )
6450        .unwrap();
6451        let dst_area = CorticalArea::new(
6452            dst_id,
6453            0,
6454            "dst".to_string(),
6455            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6456            (0, 0, 0).into(),
6457            CorticalAreaType::BrainOutput(IOCorticalAreaConfigurationFlag::Boolean),
6458        )
6459        .unwrap();
6460
6461        manager.add_cortical_area(src_area).unwrap();
6462        manager.add_cortical_area(dst_area).unwrap();
6463
6464        // Add a couple neurons to each area
6465        let s0 = manager
6466            .add_neuron(&src_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6467            .unwrap();
6468        let s1 = manager
6469            .add_neuron(&src_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6470            .unwrap();
6471        let t0 = manager
6472            .add_neuron(&dst_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6473            .unwrap();
6474        let t1 = manager
6475            .add_neuron(&dst_id, 1, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6476            .unwrap();
6477
6478        // Create synapses that represent an established mapping between the two areas
6479        manager.create_synapse(s0, t0, 128, 200, 0).unwrap();
6480        manager.create_synapse(s1, t1, 128, 200, 0).unwrap();
6481
6482        // Build index once before pruning
6483        {
6484            let mut npu = dyn_npu.lock().unwrap();
6485            npu.rebuild_synapse_index();
6486            assert_eq!(npu.get_synapse_count(), 2);
6487        }
6488
6489        // Update mapping rules (non-empty) and regenerate.
6490        // This should prune the existing A→B synapses before re-applying the mapping.
6491        //
6492        // Use "episodic_memory" morphology to avoid creating physical synapses; the key assertion is that
6493        // the pre-existing synapses were pruned on update.
6494        manager
6495            .update_cortical_mapping(
6496                &src_id,
6497                &dst_id,
6498                vec![serde_json::json!({
6499                    "morphology_id": "episodic_memory",
6500                    "morphology_scalar": [1],
6501                    "postSynapticCurrent_multiplier": 1,
6502                    "plasticity_flag": false,
6503                    "plasticity_constant": 0,
6504                    "ltp_multiplier": 0,
6505                    "ltd_multiplier": 0,
6506                    "plasticity_window": 0,
6507                })],
6508            )
6509            .unwrap();
6510        let created = manager
6511            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6512            .unwrap();
6513        assert_eq!(created, 0);
6514
6515        // Verify synapses are gone and no outgoing synapses remain from the sources
6516        {
6517            let mut npu = dyn_npu.lock().unwrap();
6518            // Pruning invalidates synapses; rebuild the index so counts/outgoing queries reflect the current state.
6519            npu.rebuild_synapse_index();
6520            assert_eq!(npu.get_synapse_count(), 0);
6521            assert!(npu.get_outgoing_synapses(s0 as u32).is_empty());
6522            assert!(npu.get_outgoing_synapses(s1 as u32).is_empty());
6523        }
6524    }
6525
6526    #[test]
6527    fn test_upstream_area_tracking() {
6528        // Test that upstream_cortical_areas property is maintained correctly
6529        use crate::models::cortical_area::CorticalArea;
6530        use feagi_npu_burst_engine::backend::CPUBackend;
6531        use feagi_npu_burst_engine::TracingMutex;
6532        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6533        use feagi_npu_runtime::StdRuntime;
6534        use feagi_structures::genomic::cortical_area::{
6535            CorticalAreaDimensions, CorticalAreaType, CorticalID,
6536        };
6537
6538        // Create test manager with NPU
6539        let runtime = StdRuntime;
6540        let backend = CPUBackend::new();
6541        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6542        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6543        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6544
6545        // Seed the morphology registry with core morphologies so mapping regeneration can run.
6546        // (new_for_testing_with_npu() intentionally starts empty.)
6547        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6548
6549        // Create source area
6550        let src_id = CorticalID::try_from_bytes(b"csrc0000").unwrap();
6551        let src_area = CorticalArea::new(
6552            src_id,
6553            0,
6554            "Source Area".to_string(),
6555            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6556            (0, 0, 0).into(),
6557            CorticalAreaType::Custom(
6558                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6559            ),
6560        )
6561        .unwrap();
6562        let src_idx = manager.add_cortical_area(src_area).unwrap();
6563
6564        // Create destination area (memory area)
6565        let dst_id = CorticalID::try_from_bytes(b"cdst0000").unwrap();
6566        let dst_area = CorticalArea::new(
6567            dst_id,
6568            0,
6569            "Dest Area".to_string(),
6570            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6571            (0, 0, 0).into(),
6572            CorticalAreaType::Custom(
6573                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6574            ),
6575        )
6576        .unwrap();
6577        manager.add_cortical_area(dst_area).unwrap();
6578
6579        // Verify upstream_cortical_areas property was initialized to empty array
6580        {
6581            let dst_area = manager.get_cortical_area(&dst_id).unwrap();
6582            let upstream = dst_area.properties.get("upstream_cortical_areas").unwrap();
6583            assert!(
6584                upstream.as_array().unwrap().is_empty(),
6585                "Upstream areas should be empty initially"
6586            );
6587        }
6588
6589        // Create a mapping from src to dst
6590        let mapping_data = vec![serde_json::json!({
6591            "morphology_id": "episodic_memory",
6592            "morphology_scalar": 1,
6593            "postSynapticCurrent_multiplier": 1.0,
6594        })];
6595        manager
6596            .update_cortical_mapping(&src_id, &dst_id, mapping_data)
6597            .unwrap();
6598        manager
6599            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6600            .unwrap();
6601
6602        // Verify src_idx was added to dst's upstream_cortical_areas
6603        {
6604            let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
6605            assert_eq!(upstream_areas.len(), 1, "Should have 1 upstream area");
6606            assert_eq!(
6607                upstream_areas[0], src_idx,
6608                "Upstream area should be src_idx"
6609            );
6610        }
6611
6612        // Delete the mapping
6613        manager
6614            .update_cortical_mapping(&src_id, &dst_id, vec![])
6615            .unwrap();
6616        manager
6617            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6618            .unwrap();
6619
6620        // Verify src_idx was removed from dst's upstream_cortical_areas
6621        {
6622            let upstream_areas = manager.get_upstream_cortical_areas(&dst_id);
6623            assert_eq!(
6624                upstream_areas.len(),
6625                0,
6626                "Should have 0 upstream areas after deletion"
6627            );
6628        }
6629    }
6630
6631    #[test]
6632    fn test_refresh_upstream_areas_for_associative_memory_pairs() {
6633        use crate::models::cortical_area::CorticalArea;
6634        use feagi_npu_burst_engine::backend::CPUBackend;
6635        use feagi_npu_burst_engine::TracingMutex;
6636        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6637        use feagi_npu_runtime::StdRuntime;
6638        use feagi_structures::genomic::cortical_area::{
6639            CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
6640        };
6641        use std::sync::Arc;
6642
6643        let runtime = StdRuntime;
6644        let backend = CPUBackend::new();
6645        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6646        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6647        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6648        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6649
6650        let a1_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
6651        let a2_id = CorticalID::try_from_bytes(b"csrc0003").unwrap();
6652        let m1_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
6653        let m2_id = CorticalID::try_from_bytes(b"mmem0003").unwrap();
6654
6655        let a1_area = CorticalArea::new(
6656            a1_id,
6657            0,
6658            "A1".to_string(),
6659            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6660            (0, 0, 0).into(),
6661            CorticalAreaType::Custom(
6662                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6663            ),
6664        )
6665        .unwrap();
6666        let a2_area = CorticalArea::new(
6667            a2_id,
6668            0,
6669            "A2".to_string(),
6670            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6671            (0, 0, 0).into(),
6672            CorticalAreaType::Custom(
6673                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
6674            ),
6675        )
6676        .unwrap();
6677
6678        let mut m1_area = CorticalArea::new(
6679            m1_id,
6680            0,
6681            "M1".to_string(),
6682            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6683            (0, 0, 0).into(),
6684            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6685        )
6686        .unwrap();
6687        m1_area
6688            .properties
6689            .insert("is_mem_type".to_string(), serde_json::json!(true));
6690        m1_area
6691            .properties
6692            .insert("temporal_depth".to_string(), serde_json::json!(1));
6693
6694        let mut m2_area = CorticalArea::new(
6695            m2_id,
6696            0,
6697            "M2".to_string(),
6698            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6699            (0, 0, 0).into(),
6700            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6701        )
6702        .unwrap();
6703        m2_area
6704            .properties
6705            .insert("is_mem_type".to_string(), serde_json::json!(true));
6706        m2_area
6707            .properties
6708            .insert("temporal_depth".to_string(), serde_json::json!(1));
6709
6710        let a1_idx = manager.add_cortical_area(a1_area).unwrap();
6711        let a2_idx = manager.add_cortical_area(a2_area).unwrap();
6712        let m1_idx = manager.add_cortical_area(m1_area).unwrap();
6713        let m2_idx = manager.add_cortical_area(m2_area).unwrap();
6714
6715        manager
6716            .add_neuron(&a1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6717            .unwrap();
6718        manager
6719            .add_neuron(&a2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6720            .unwrap();
6721
6722        let episodic_mapping = vec![serde_json::json!({
6723            "morphology_id": "episodic_memory",
6724            "morphology_scalar": 1,
6725            "postSynapticCurrent_multiplier": 1.0,
6726        })];
6727        manager
6728            .update_cortical_mapping(&a1_id, &m1_id, episodic_mapping.clone())
6729            .unwrap();
6730        manager
6731            .regenerate_synapses_for_mapping(&a1_id, &m1_id)
6732            .unwrap();
6733        manager
6734            .update_cortical_mapping(&a2_id, &m2_id, episodic_mapping)
6735            .unwrap();
6736        manager
6737            .regenerate_synapses_for_mapping(&a2_id, &m2_id)
6738            .unwrap();
6739
6740        let assoc_mapping = vec![serde_json::json!({
6741            "morphology_id": "associative_memory",
6742            "morphology_scalar": 1,
6743            "postSynapticCurrent_multiplier": 1.0,
6744            "plasticity_flag": true,
6745            "plasticity_constant": 1,
6746            "ltp_multiplier": 1,
6747            "ltd_multiplier": 1,
6748            "plasticity_window": 5,
6749        })];
6750        manager
6751            .update_cortical_mapping(&m1_id, &m2_id, assoc_mapping)
6752            .unwrap();
6753        manager
6754            .regenerate_synapses_for_mapping(&m1_id, &m2_id)
6755            .unwrap();
6756
6757        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
6758        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
6759        assert_eq!(
6760            upstream_m1.len(),
6761            1,
6762            "M1 should have only 1 upstream before refresh"
6763        );
6764        assert_eq!(
6765            upstream_m2.len(),
6766            2,
6767            "M2 should have 2 upstreams before refresh"
6768        );
6769
6770        manager.refresh_upstream_cortical_areas_from_mappings(&m1_id);
6771        manager.refresh_upstream_cortical_areas_from_mappings(&m2_id);
6772
6773        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
6774        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
6775        assert_eq!(
6776            upstream_m1.len(),
6777            2,
6778            "M1 should have 2 upstreams after refresh"
6779        );
6780        assert_eq!(
6781            upstream_m2.len(),
6782            2,
6783            "M2 should have 2 upstreams after refresh"
6784        );
6785        assert!(upstream_m1.contains(&a1_idx));
6786        assert!(upstream_m1.contains(&m2_idx));
6787        assert!(upstream_m2.contains(&a2_idx));
6788        assert!(upstream_m2.contains(&m1_idx));
6789
6790        // Fire upstream neurons and ensure burst processing works without altering upstream tracking.
6791        {
6792            let mut npu_lock = dyn_npu.lock().unwrap();
6793            let injected_a1 = npu_lock.inject_sensory_xyzp_by_id(&a1_id, &[(0, 0, 0, 1.0)]);
6794            let injected_a2 = npu_lock.inject_sensory_xyzp_by_id(&a2_id, &[(0, 0, 0, 1.0)]);
6795            assert_eq!(injected_a1, 1, "Expected A1 injection to match one neuron");
6796            assert_eq!(injected_a2, 1, "Expected A2 injection to match one neuron");
6797            npu_lock.process_burst().expect("Burst processing failed");
6798        }
6799
6800        let upstream_m1 = manager.get_upstream_cortical_areas(&m1_id);
6801        let upstream_m2 = manager.get_upstream_cortical_areas(&m2_id);
6802        assert_eq!(
6803            upstream_m1.len(),
6804            2,
6805            "M1 should keep 2 upstreams after firing"
6806        );
6807        assert_eq!(
6808            upstream_m2.len(),
6809            2,
6810            "M2 should keep 2 upstreams after firing"
6811        );
6812    }
6813
6814    #[test]
6815    fn test_memory_twin_created_for_memory_mapping() {
6816        use crate::models::cortical_area::CorticalArea;
6817        use feagi_npu_burst_engine::backend::CPUBackend;
6818        use feagi_npu_burst_engine::TracingMutex;
6819        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6820        use feagi_npu_runtime::StdRuntime;
6821        use feagi_structures::genomic::cortical_area::{
6822            CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
6823            MemoryCorticalType,
6824        };
6825        use std::sync::Arc;
6826
6827        let runtime = StdRuntime;
6828        let backend = CPUBackend::new();
6829        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6830        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6831        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6832        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6833
6834        let src_id = CorticalID::try_from_bytes(b"csrc0001").unwrap();
6835        let dst_id = CorticalID::try_from_bytes(b"mmem0001").unwrap();
6836
6837        let src_area = CorticalArea::new(
6838            src_id,
6839            0,
6840            "Source Area".to_string(),
6841            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6842            (0, 0, 0).into(),
6843            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
6844        )
6845        .unwrap();
6846        let mut dst_area = CorticalArea::new(
6847            dst_id,
6848            0,
6849            "Memory Area".to_string(),
6850            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
6851            (0, 0, 0).into(),
6852            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6853        )
6854        .unwrap();
6855        dst_area
6856            .properties
6857            .insert("is_mem_type".to_string(), serde_json::json!(true));
6858        dst_area
6859            .properties
6860            .insert("temporal_depth".to_string(), serde_json::json!(1));
6861
6862        manager.add_cortical_area(src_area).unwrap();
6863        manager.add_cortical_area(dst_area).unwrap();
6864
6865        let mapping_data = vec![serde_json::json!({
6866            "morphology_id": "episodic_memory",
6867            "morphology_scalar": 1,
6868            "postSynapticCurrent_multiplier": 1.0,
6869        })];
6870        manager
6871            .update_cortical_mapping(&src_id, &dst_id, mapping_data)
6872            .unwrap();
6873        manager
6874            .regenerate_synapses_for_mapping(&src_id, &dst_id)
6875            .unwrap();
6876
6877        let memory_area = manager.get_cortical_area(&dst_id).unwrap();
6878        let twin_map = memory_area
6879            .properties
6880            .get("memory_twin_areas")
6881            .and_then(|v| v.as_object())
6882            .expect("memory_twin_areas should be set");
6883        let twin_id_str = twin_map
6884            .get(&src_id.as_base_64())
6885            .and_then(|v| v.as_str())
6886            .expect("Missing twin entry for upstream area");
6887        let twin_id = CorticalID::try_from_base_64(twin_id_str).unwrap();
6888        let mapping = memory_area
6889            .properties
6890            .get("cortical_mapping_dst")
6891            .and_then(|v| v.as_object())
6892            .and_then(|map| map.get(&twin_id.as_base_64()))
6893            .and_then(|v| v.as_array())
6894            .expect("Missing memory replay mapping for twin area");
6895        let uses_replay = mapping.iter().any(|rule| {
6896            rule.get("morphology_id")
6897                .and_then(|v| v.as_str())
6898                .is_some_and(|id| id == "memory_replay")
6899        });
6900        assert!(uses_replay, "Expected memory_replay mapping for twin area");
6901
6902        let twin_area = manager.get_cortical_area(&twin_id).unwrap();
6903        assert!(matches!(
6904            twin_area.cortical_type,
6905            CorticalAreaType::Custom(_)
6906        ));
6907        assert_eq!(
6908            twin_area
6909                .properties
6910                .get("memory_twin_of")
6911                .and_then(|v| v.as_str()),
6912            Some(src_id.as_base_64().as_str())
6913        );
6914        assert_eq!(
6915            twin_area
6916                .properties
6917                .get("memory_twin_for")
6918                .and_then(|v| v.as_str()),
6919            Some(dst_id.as_base_64().as_str())
6920        );
6921    }
6922
6923    #[test]
6924    fn test_associative_memory_between_memory_areas_creates_synapses() {
6925        use crate::models::cortical_area::CorticalArea;
6926        use feagi_npu_burst_engine::backend::CPUBackend;
6927        use feagi_npu_burst_engine::TracingMutex;
6928        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
6929        use feagi_npu_runtime::StdRuntime;
6930        use feagi_structures::genomic::cortical_area::{
6931            CorticalAreaDimensions, CorticalAreaType, CorticalID, MemoryCorticalType,
6932        };
6933        use std::sync::Arc;
6934
6935        let runtime = StdRuntime;
6936        let backend = CPUBackend::new();
6937        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
6938        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
6939        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
6940        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
6941
6942        let m1_id = CorticalID::try_from_bytes(b"mmem0402").unwrap();
6943        let m2_id = CorticalID::try_from_bytes(b"mmem0403").unwrap();
6944
6945        let mut m1_area = CorticalArea::new(
6946            m1_id,
6947            0,
6948            "Memory M1".to_string(),
6949            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6950            (0, 0, 0).into(),
6951            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6952        )
6953        .unwrap();
6954        m1_area
6955            .properties
6956            .insert("is_mem_type".to_string(), serde_json::json!(true));
6957        m1_area
6958            .properties
6959            .insert("temporal_depth".to_string(), serde_json::json!(1));
6960
6961        let mut m2_area = CorticalArea::new(
6962            m2_id,
6963            0,
6964            "Memory M2".to_string(),
6965            CorticalAreaDimensions::new(1, 1, 1).unwrap(),
6966            (0, 0, 0).into(),
6967            CorticalAreaType::Memory(MemoryCorticalType::Memory),
6968        )
6969        .unwrap();
6970        m2_area
6971            .properties
6972            .insert("is_mem_type".to_string(), serde_json::json!(true));
6973        m2_area
6974            .properties
6975            .insert("temporal_depth".to_string(), serde_json::json!(1));
6976
6977        manager.add_cortical_area(m1_area).unwrap();
6978        manager.add_cortical_area(m2_area).unwrap();
6979
6980        manager
6981            .add_neuron(&m1_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6982            .unwrap();
6983        manager
6984            .add_neuron(&m2_id, 0, 0, 0, 1.0, 0.0, 0.1, 0.0, 0, 1, 1.0, 3, 1, false)
6985            .unwrap();
6986
6987        let mapping_data = vec![serde_json::json!({
6988            "morphology_id": "associative_memory",
6989            "morphology_scalar": 1,
6990            "postSynapticCurrent_multiplier": 1.0,
6991            "plasticity_flag": true,
6992            "plasticity_constant": 1,
6993            "ltp_multiplier": 1,
6994            "ltd_multiplier": 1,
6995            "plasticity_window": 5,
6996        })];
6997        manager
6998            .update_cortical_mapping(&m1_id, &m2_id, mapping_data)
6999            .unwrap();
7000        let created = manager
7001            .regenerate_synapses_for_mapping(&m1_id, &m2_id)
7002            .unwrap();
7003        assert!(
7004            created > 0,
7005            "Expected associative memory mapping between memory areas to create synapses"
7006        );
7007    }
7008
7009    #[test]
7010    fn test_memory_twin_repair_on_load_preserves_replay_mapping() {
7011        use crate::models::cortical_area::CorticalArea;
7012        use feagi_npu_burst_engine::backend::CPUBackend;
7013        use feagi_npu_burst_engine::TracingMutex;
7014        use feagi_npu_burst_engine::{DynamicNPU, RustNPU};
7015        use feagi_npu_runtime::StdRuntime;
7016        use feagi_structures::genomic::cortical_area::{
7017            CorticalAreaDimensions, CorticalAreaType, CorticalID, IOCorticalAreaConfigurationFlag,
7018            MemoryCorticalType,
7019        };
7020        use std::sync::Arc;
7021
7022        let runtime = StdRuntime;
7023        let backend = CPUBackend::new();
7024        let npu = RustNPU::new(runtime, backend, 10_000, 10_000, 10).expect("Failed to create NPU");
7025        let dyn_npu = Arc::new(TracingMutex::new(DynamicNPU::F32(npu), "TestNPU"));
7026        let mut manager = ConnectomeManager::new_for_testing_with_npu(dyn_npu.clone());
7027        feagi_evolutionary::templates::add_core_morphologies(&mut manager.morphology_registry);
7028
7029        let src_id = CorticalID::try_from_bytes(b"csrc0002").unwrap();
7030        let mem_id = CorticalID::try_from_bytes(b"mmem0002").unwrap();
7031
7032        let src_area = CorticalArea::new(
7033            src_id,
7034            0,
7035            "Source Area".to_string(),
7036            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7037            (0, 0, 0).into(),
7038            CorticalAreaType::BrainInput(IOCorticalAreaConfigurationFlag::Boolean),
7039        )
7040        .unwrap();
7041        let mut mem_area = CorticalArea::new(
7042            mem_id,
7043            0,
7044            "Memory Area".to_string(),
7045            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7046            (0, 0, 0).into(),
7047            CorticalAreaType::Memory(MemoryCorticalType::Memory),
7048        )
7049        .unwrap();
7050        mem_area
7051            .properties
7052            .insert("is_mem_type".to_string(), serde_json::json!(true));
7053        mem_area
7054            .properties
7055            .insert("temporal_depth".to_string(), serde_json::json!(1));
7056
7057        manager.add_cortical_area(src_area).unwrap();
7058        manager.add_cortical_area(mem_area).unwrap();
7059
7060        let twin_id = manager
7061            .build_memory_twin_id(&mem_id, &src_id)
7062            .expect("Failed to build twin id");
7063        let twin_area = CorticalArea::new(
7064            twin_id,
7065            0,
7066            "Source Area_twin".to_string(),
7067            CorticalAreaDimensions::new(2, 2, 1).unwrap(),
7068            (0, 0, 0).into(),
7069            CorticalAreaType::Custom(
7070                feagi_structures::genomic::cortical_area::CustomCorticalType::LeakyIntegrateFire,
7071            ),
7072        )
7073        .unwrap();
7074        manager.add_cortical_area(twin_area).unwrap();
7075
7076        let repaired = manager
7077            .ensure_memory_twin_area(&mem_id, &src_id)
7078            .expect("Failed to repair twin");
7079        assert_eq!(repaired, twin_id);
7080
7081        let mem_area = manager.get_cortical_area(&mem_id).unwrap();
7082        let twin_map = mem_area
7083            .properties
7084            .get("memory_twin_areas")
7085            .and_then(|v| v.as_object())
7086            .expect("memory_twin_areas should be set");
7087        let twin_id_str = twin_map
7088            .get(&src_id.as_base_64())
7089            .and_then(|v| v.as_str())
7090            .expect("Missing twin entry for upstream area");
7091        assert_eq!(twin_id_str, twin_id.as_base_64());
7092
7093        let replay_map = mem_area
7094            .properties
7095            .get("cortical_mapping_dst")
7096            .and_then(|v| v.as_object())
7097            .and_then(|map| map.get(&twin_id.as_base_64()))
7098            .and_then(|v| v.as_array())
7099            .expect("Missing memory replay mapping for twin area");
7100        let uses_replay = replay_map.iter().any(|rule| {
7101            rule.get("morphology_id")
7102                .and_then(|v| v.as_str())
7103                .is_some_and(|id| id == "memory_replay")
7104        });
7105        assert!(uses_replay, "Expected memory_replay mapping for twin area");
7106
7107        let twin_area = manager.get_cortical_area(&twin_id).unwrap();
7108        assert_eq!(
7109            twin_area
7110                .properties
7111                .get("memory_twin_of")
7112                .and_then(|v| v.as_str()),
7113            Some(src_id.as_base_64().as_str())
7114        );
7115        assert_eq!(
7116            twin_area
7117                .properties
7118                .get("memory_twin_for")
7119                .and_then(|v| v.as_str()),
7120            Some(mem_id.as_base_64().as_str())
7121        );
7122    }
7123}