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