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