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