Skip to main content

feagi_brain_development/
connectome_manager.rs

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