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