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