Skip to main content

feagi_brain_development/
connectome_manager.rs

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