Skip to main content

feagi_brain_development/
connectome_manager.rs

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