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