Skip to main content

feagi_brain_development/
connectome_manager.rs

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