Skip to main content

feagi_api/endpoints/
genome.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! Genome API Endpoints - Exact port from Python `/v1/genome/*`
5
6// Removed - using crate::common::State instead
7use crate::amalgamation;
8use crate::common::ApiState;
9use crate::common::{ApiError, ApiResult, Json, Query, State};
10use feagi_services::types::{GenomeInfo, LoadGenomeParams};
11use std::collections::HashMap;
12use std::sync::atomic::Ordering;
13use std::sync::Arc;
14use tracing::info;
15use uuid::Uuid;
16
17#[cfg(feature = "http")]
18use axum::extract::Multipart;
19
20/// Multipart file upload schema for Swagger UI.
21///
22/// This enables Swagger to show a file picker for endpoints that accept genome JSON files.
23#[derive(Debug, Clone, utoipa::ToSchema)]
24pub struct GenomeFileUploadForm {
25    /// Genome JSON file contents.
26    #[schema(value_type = String, format = Binary)]
27    pub file: String,
28}
29
30fn queue_amalgamation_from_genome_json_str(
31    state: &ApiState,
32    genome_json: String,
33) -> Result<String, ApiError> {
34    // Only one pending amalgamation is supported per FEAGI session (matches BV workflow).
35    {
36        let lock = state.amalgamation_state.read();
37        if lock.pending.is_some() {
38            return Err(ApiError::invalid_input(
39                "Amalgamation already pending; cancel it first via /v1/genome/amalgamation_cancellation",
40            ));
41        }
42    }
43
44    let genome = feagi_evolutionary::load_genome_from_json(&genome_json)
45        .map_err(|e| ApiError::invalid_input(format!("Invalid genome payload: {}", e)))?;
46
47    let circuit_size = amalgamation::compute_circuit_size_from_runtime_genome(&genome);
48
49    let amalgamation_id = Uuid::new_v4().to_string();
50    let genome_title = genome.metadata.genome_title.clone();
51
52    let summary = amalgamation::AmalgamationPendingSummary {
53        amalgamation_id: amalgamation_id.clone(),
54        genome_title,
55        circuit_size,
56    };
57
58    let pending = amalgamation::AmalgamationPending {
59        summary: summary.clone(),
60        genome_json,
61    };
62
63    {
64        let mut lock = state.amalgamation_state.write();
65        let now_ms = std::time::SystemTime::now()
66            .duration_since(std::time::UNIX_EPOCH)
67            .map(|d| d.as_millis() as i64)
68            .unwrap_or(0);
69
70        lock.history.push(amalgamation::AmalgamationHistoryEntry {
71            amalgamation_id: summary.amalgamation_id.clone(),
72            genome_title: summary.genome_title.clone(),
73            circuit_size: summary.circuit_size,
74            status: "pending".to_string(),
75            timestamp_ms: now_ms,
76        });
77        lock.pending = Some(pending);
78    }
79
80    tracing::info!(
81        target: "feagi-api",
82        "🧬 [AMALGAMATION] Queued pending amalgamation id={} title='{}' circuit_size={:?}",
83        summary.amalgamation_id,
84        summary.genome_title,
85        summary.circuit_size
86    );
87
88    Ok(amalgamation_id)
89}
90
91struct GenomeTransitionFlagGuard {
92    in_progress: Arc<std::sync::atomic::AtomicBool>,
93}
94
95impl Drop for GenomeTransitionFlagGuard {
96    fn drop(&mut self) {
97        self.in_progress.store(false, Ordering::SeqCst);
98    }
99}
100
101/// Execute a genome load with strict priority over concurrent operations.
102///
103/// Guarantees:
104/// - Only one genome transition may run at a time.
105/// - Runtime is quiesced before load starts.
106/// - Runtime frequency is updated from genome physiology.
107/// - Runtime is restored to running state if it was running before transition.
108async fn load_genome_with_priority(
109    state: &ApiState,
110    params: LoadGenomeParams,
111    source: &str,
112) -> ApiResult<GenomeInfo> {
113    let _transition_lock = state.genome_transition_lock.try_lock().map_err(|_| {
114        ApiError::conflict(
115            "Another genome transition is already in progress; wait for it to finish",
116        )
117    })?;
118    state
119        .genome_transition_in_progress
120        .store(true, Ordering::SeqCst);
121    let _guard = GenomeTransitionFlagGuard {
122        in_progress: Arc::clone(&state.genome_transition_in_progress),
123    };
124
125    tracing::info!(
126        target: "feagi-api",
127        "🛑 Entering prioritized genome transition from {}",
128        source
129    );
130
131    let runtime_service = state.runtime_service.as_ref();
132    #[cfg(feature = "feagi-agent")]
133    if let Some(handler) = &state.agent_handler {
134        let deregistered_ids = {
135            let mut guard = handler.lock().unwrap();
136            guard.force_deregister_all_agents("forced by genome transition")
137        };
138        for agent_id in &deregistered_ids {
139            runtime_service.unregister_motor_subscriptions(agent_id);
140            runtime_service.unregister_visualization_subscriptions(agent_id);
141        }
142        tracing::info!(
143            target: "feagi-api",
144            "🔌 Forced deregistration for {} agents before genome transition",
145            deregistered_ids.len()
146        );
147    }
148    // Strict transition barrier: guarantee no stale subscriptions survive.
149    runtime_service.clear_all_motor_subscriptions();
150    runtime_service.clear_all_visualization_subscriptions();
151
152    let runtime_status = runtime_service
153        .get_status()
154        .await
155        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
156    let runtime_was_running = runtime_status.is_running;
157
158    if runtime_was_running {
159        tracing::info!(
160            target: "feagi-api",
161            "Stopping burst engine before prioritized genome transition"
162        );
163        runtime_service.stop().await.map_err(|e| {
164            ApiError::internal(format!(
165                "Failed to stop burst engine before genome transition: {}",
166                e
167            ))
168        })?;
169    }
170
171    let genome_service = state.genome_service.as_ref();
172    let load_result = genome_service.load_genome(params).await;
173    let genome_info = match load_result {
174        Ok(info) => info,
175        Err(e) => {
176            if runtime_was_running {
177                if let Err(restart_err) = runtime_service.start().await {
178                    tracing::warn!(
179                        target: "feagi-api",
180                        "Failed to restore runtime after failed genome load (source={}): {}",
181                        source,
182                        restart_err
183                    );
184                }
185            }
186            return Err(ApiError::internal(format!("Failed to load genome: {}", e)));
187        }
188    };
189
190    let burst_frequency_hz = 1.0 / genome_info.simulation_timestep;
191    runtime_service
192        .set_frequency(burst_frequency_hz)
193        .await
194        .map_err(|e| ApiError::internal(format!("Failed to update burst frequency: {}", e)))?;
195
196    if runtime_was_running {
197        runtime_service.start().await.map_err(|e| {
198            ApiError::internal(format!(
199                "Failed to restart burst engine after genome transition: {}",
200                e
201            ))
202        })?;
203    }
204
205    tracing::info!(
206        target: "feagi-api",
207        "✅ Prioritized genome transition completed from {}",
208        source
209    );
210
211    // Deterministic: create missing IO areas for any registered agents immediately after genome load.
212    // Fixes nondeterministic behavior where areas were missing on first run but appeared on restart.
213    #[cfg(feature = "feagi-agent")]
214    if let Some(handler) = &state.agent_handler {
215        let device_regs_list: Vec<serde_json::Value> = {
216            let guard = handler.lock().unwrap();
217            guard
218                .get_all_registered_agents()
219                .iter()
220                .filter_map(|(sid, _)| guard.get_device_registrations_by_agent(*sid).cloned())
221                .collect()
222        };
223        for device_regs in device_regs_list {
224            crate::common::agent_registration::auto_create_cortical_areas_from_device_registrations(
225                state,
226                &device_regs,
227            )
228            .await;
229        }
230    }
231
232    Ok(genome_info)
233}
234
235/// Inject the current runtime simulation timestep (seconds) into a genome JSON value.
236///
237/// Rationale: the burst engine timestep can be updated at runtime, but `GenomeService::save_genome()`
238/// serializes the stored `RuntimeGenome` (which may still have the older physiology value).
239/// This keeps exported/saved genomes consistent with the *current* FEAGI simulation state.
240fn inject_simulation_timestep_into_genome(
241    mut genome: serde_json::Value,
242    simulation_timestep_s: f64,
243) -> Result<serde_json::Value, ApiError> {
244    let physiology = genome
245        .get_mut("physiology")
246        .and_then(|v| v.as_object_mut())
247        .ok_or_else(|| {
248            ApiError::internal(
249                "Genome JSON missing required object key 'physiology' while saving".to_string(),
250            )
251        })?;
252
253    physiology.insert(
254        "simulation_timestep".to_string(),
255        serde_json::Value::from(simulation_timestep_s),
256    );
257    Ok(genome)
258}
259
260async fn get_current_runtime_simulation_timestep_s(state: &ApiState) -> Result<f64, ApiError> {
261    let runtime_service = state.runtime_service.as_ref();
262    let status = runtime_service
263        .get_status()
264        .await
265        .map_err(|e| ApiError::internal(format!("Failed to get runtime status: {}", e)))?;
266
267    // Convert frequency (Hz) to timestep (seconds).
268    Ok(if status.frequency_hz > 0.0 {
269        1.0 / status.frequency_hz
270    } else {
271        0.0
272    })
273}
274
275/// Get the current genome file name.
276#[utoipa::path(get, path = "/v1/genome/file_name", tag = "genome")]
277pub async fn get_file_name(
278    State(_state): State<ApiState>,
279) -> ApiResult<Json<HashMap<String, String>>> {
280    // TODO: Get current genome filename
281    Ok(Json(HashMap::from([(
282        "genome_file_name".to_string(),
283        "".to_string(),
284    )])))
285}
286
287/// Get list of available circuit templates from the circuit library.
288#[utoipa::path(get, path = "/v1/genome/circuits", tag = "genome")]
289pub async fn get_circuits(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
290    // TODO: Get available circuit library
291    Ok(Json(vec![]))
292}
293
294/// Set the destination for genome amalgamation (merging genomes).
295#[utoipa::path(post, path = "/v1/genome/amalgamation_destination", tag = "genome")]
296pub async fn post_amalgamation_destination(
297    State(state): State<ApiState>,
298    Query(params): Query<HashMap<String, String>>,
299    Json(req): Json<HashMap<String, serde_json::Value>>,
300) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
301    // BV sends query params:
302    // - circuit_origin_x/y/z
303    // - amalgamation_id
304    // - rewire_mode
305    //
306    // Body:
307    // - brain_region_id
308    let amalgamation_id = params
309        .get("amalgamation_id")
310        .ok_or_else(|| ApiError::invalid_input("amalgamation_id required"))?
311        .to_string();
312
313    let origin_x: i32 = params
314        .get("circuit_origin_x")
315        .ok_or_else(|| ApiError::invalid_input("circuit_origin_x required"))?
316        .parse()
317        .map_err(|_| ApiError::invalid_input("circuit_origin_x must be an integer"))?;
318    let origin_y: i32 = params
319        .get("circuit_origin_y")
320        .ok_or_else(|| ApiError::invalid_input("circuit_origin_y required"))?
321        .parse()
322        .map_err(|_| ApiError::invalid_input("circuit_origin_y must be an integer"))?;
323    let origin_z: i32 = params
324        .get("circuit_origin_z")
325        .ok_or_else(|| ApiError::invalid_input("circuit_origin_z required"))?
326        .parse()
327        .map_err(|_| ApiError::invalid_input("circuit_origin_z must be an integer"))?;
328
329    let rewire_mode = params
330        .get("rewire_mode")
331        .cloned()
332        .unwrap_or_else(|| "rewire_all".to_string());
333
334    let parent_region_id = req
335        .get("brain_region_id")
336        .and_then(|v| v.as_str())
337        .ok_or_else(|| ApiError::invalid_input("brain_region_id required"))?
338        .to_string();
339
340    // Resolve and consume the pending request.
341    let pending = {
342        let lock = state.amalgamation_state.write();
343        let Some(p) = lock.pending.as_ref() else {
344            return Err(ApiError::invalid_input("No amalgamation is pending"));
345        };
346        if p.summary.amalgamation_id != amalgamation_id {
347            return Err(ApiError::invalid_input(format!(
348                "Pending amalgamation_id mismatch: expected {}, got {}",
349                p.summary.amalgamation_id, amalgamation_id
350            )));
351        }
352        p.clone()
353    };
354
355    // 1) Create a new brain region to host the imported circuit.
356    // Note: ConnectomeServiceImpl shares the same RuntimeGenome Arc with GenomeServiceImpl, so
357    // persisting the region into the RuntimeGenome is required for subsequent cortical-area creation.
358    let connectome_service = state.connectome_service.as_ref();
359
360    let mut region_properties: HashMap<String, serde_json::Value> = HashMap::new();
361    region_properties.insert(
362        "coordinate_3d".to_string(),
363        serde_json::json!([origin_x, origin_y, origin_z]),
364    );
365    region_properties.insert(
366        "amalgamation_id".to_string(),
367        serde_json::json!(pending.summary.amalgamation_id),
368    );
369    region_properties.insert(
370        "circuit_size".to_string(),
371        serde_json::json!(pending.summary.circuit_size),
372    );
373    region_properties.insert("rewire_mode".to_string(), serde_json::json!(rewire_mode));
374
375    connectome_service
376        .create_brain_region(feagi_services::types::CreateBrainRegionParams {
377            region_id: amalgamation_id.clone(),
378            name: pending.summary.genome_title.clone(),
379            region_type: "Custom".to_string(),
380            parent_id: Some(parent_region_id.clone()),
381            properties: Some(region_properties),
382        })
383        .await
384        .map_err(|e| {
385            ApiError::internal(format!("Failed to create amalgamation brain region: {}", e))
386        })?;
387
388    // 2) Import cortical areas into that region.
389    //
390    // Current deterministic behavior:
391    // - We import *only* cortical areas whose IDs do not exist in the current connectome.
392    // - We place them at an offset relative to the chosen origin.
393    // - We assign parent_region_id to the new region so the genome stays consistent.
394    //
395    // If a genome contains shared/global IDs (e.g., core areas), those will be skipped.
396    let imported_genome =
397        feagi_evolutionary::load_genome_from_json(&pending.genome_json).map_err(|e| {
398            ApiError::invalid_input(format!(
399                "Pending genome payload can no longer be parsed as a genome: {}",
400                e
401            ))
402        })?;
403
404    let genome_service = state.genome_service.as_ref();
405    let mut to_create: Vec<feagi_services::types::CreateCorticalAreaParams> = Vec::new();
406    let mut skipped_existing: Vec<String> = Vec::new();
407
408    for area in imported_genome.cortical_areas.values() {
409        let cortical_id = area.cortical_id.as_base_64();
410        let exists = connectome_service
411            .cortical_area_exists(&cortical_id)
412            .await
413            .map_err(|e| {
414                ApiError::internal(format!(
415                    "Failed to check existing cortical area {}: {}",
416                    cortical_id, e
417                ))
418            })?;
419        if exists {
420            skipped_existing.push(cortical_id);
421            continue;
422        }
423
424        let mut props = area.properties.clone();
425        props.insert(
426            "parent_region_id".to_string(),
427            serde_json::json!(amalgamation_id.clone()),
428        );
429        props.insert(
430            "amalgamation_source".to_string(),
431            serde_json::json!("amalgamation_by_payload"),
432        );
433
434        to_create.push(feagi_services::types::CreateCorticalAreaParams {
435            cortical_id,
436            name: area.name.clone(),
437            dimensions: (
438                area.dimensions.width as usize,
439                area.dimensions.height as usize,
440                area.dimensions.depth as usize,
441            ),
442            position: (
443                origin_x.saturating_add(area.position.x),
444                origin_y.saturating_add(area.position.y),
445                origin_z.saturating_add(area.position.z),
446            ),
447            area_type: "Custom".to_string(),
448            visible: Some(true),
449            sub_group: None,
450            neurons_per_voxel: area
451                .properties
452                .get("neurons_per_voxel")
453                .and_then(|v| v.as_u64())
454                .map(|v| v as u32),
455            postsynaptic_current: area
456                .properties
457                .get("postsynaptic_current")
458                .and_then(|v| v.as_f64()),
459            plasticity_constant: area
460                .properties
461                .get("plasticity_constant")
462                .and_then(|v| v.as_f64()),
463            degeneration: area.properties.get("degeneration").and_then(|v| v.as_f64()),
464            psp_uniform_distribution: area
465                .properties
466                .get("psp_uniform_distribution")
467                .and_then(|v| v.as_bool()),
468            firing_threshold_increment: None,
469            firing_threshold_limit: area
470                .properties
471                .get("firing_threshold_limit")
472                .and_then(|v| v.as_f64()),
473            consecutive_fire_count: area
474                .properties
475                .get("consecutive_fire_limit")
476                .and_then(|v| v.as_u64())
477                .map(|v| v as u32),
478            snooze_period: area
479                .properties
480                .get("snooze_period")
481                .and_then(|v| v.as_u64())
482                .map(|v| v as u32),
483            refractory_period: area
484                .properties
485                .get("refractory_period")
486                .and_then(|v| v.as_u64())
487                .map(|v| v as u32),
488            leak_coefficient: area
489                .properties
490                .get("leak_coefficient")
491                .and_then(|v| v.as_f64()),
492            leak_variability: area
493                .properties
494                .get("leak_variability")
495                .and_then(|v| v.as_f64()),
496            burst_engine_active: area
497                .properties
498                .get("burst_engine_active")
499                .and_then(|v| v.as_bool()),
500            properties: Some(props),
501        });
502    }
503
504    if !to_create.is_empty() {
505        genome_service
506            .create_cortical_areas(to_create)
507            .await
508            .map_err(|e| ApiError::internal(format!("Failed to import cortical areas: {}", e)))?;
509    }
510
511    // Clear pending + write history entry
512    {
513        let mut lock = state.amalgamation_state.write();
514        let now_ms = std::time::SystemTime::now()
515            .duration_since(std::time::UNIX_EPOCH)
516            .map(|d| d.as_millis() as i64)
517            .unwrap_or(0);
518        lock.history.push(amalgamation::AmalgamationHistoryEntry {
519            amalgamation_id: pending.summary.amalgamation_id.clone(),
520            genome_title: pending.summary.genome_title.clone(),
521            circuit_size: pending.summary.circuit_size,
522            status: "confirmed".to_string(),
523            timestamp_ms: now_ms,
524        });
525        lock.pending = None;
526    }
527
528    tracing::info!(
529        target: "feagi-api",
530        "🧬 [AMALGAMATION] Confirmed and cleared pending amalgamation id={} imported_areas={} skipped_existing_areas={}",
531        pending.summary.amalgamation_id,
532        if skipped_existing.is_empty() { "unknown".to_string() } else { "partial".to_string() },
533        skipped_existing.len()
534    );
535
536    // Build a BV-compatible list response for brain regions (regions_members-like data, but as list).
537    let regions = state
538        .connectome_service
539        .list_brain_regions()
540        .await
541        .map_err(|e| ApiError::internal(format!("Failed to list brain regions: {}", e)))?;
542
543    let mut brain_regions: Vec<serde_json::Value> = Vec::new();
544    for region in regions {
545        // Shape matches BV expectations in FEAGIRequests.gd
546        let coordinate_3d = region
547            .properties
548            .get("coordinate_3d")
549            .cloned()
550            .unwrap_or_else(|| serde_json::json!([0, 0, 0]));
551        let coordinate_2d = region
552            .properties
553            .get("coordinate_2d")
554            .cloned()
555            .unwrap_or_else(|| serde_json::json!([0, 0]));
556
557        brain_regions.push(serde_json::json!({
558            "region_id": region.region_id,
559            "title": region.name,
560            "description": "",
561            "parent_region_id": region.parent_id,
562            "coordinate_2d": coordinate_2d,
563            "coordinate_3d": coordinate_3d,
564            "areas": region.cortical_areas,
565            "regions": region.child_regions,
566            "inputs": region.properties.get("inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
567            "outputs": region.properties.get("outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
568            "designated_inputs": region.properties.get("designated_inputs").cloned().unwrap_or_else(|| serde_json::json!([])),
569            "designated_outputs": region.properties.get("designated_outputs").cloned().unwrap_or_else(|| serde_json::json!([])),
570        }));
571    }
572
573    Ok(Json(HashMap::from([
574        (
575            "message".to_string(),
576            serde_json::Value::String("Amalgamation confirmed".to_string()),
577        ),
578        (
579            "brain_regions".to_string(),
580            serde_json::Value::Array(brain_regions),
581        ),
582        (
583            "skipped_existing_areas".to_string(),
584            serde_json::json!(skipped_existing),
585        ),
586    ])))
587}
588
589/// Cancel a pending genome amalgamation operation.
590#[utoipa::path(delete, path = "/v1/genome/amalgamation_cancellation", tag = "genome")]
591pub async fn delete_amalgamation_cancellation(
592    State(state): State<ApiState>,
593) -> ApiResult<Json<HashMap<String, String>>> {
594    let mut lock = state.amalgamation_state.write();
595    if let Some(pending) = lock.pending.take() {
596        let now_ms = std::time::SystemTime::now()
597            .duration_since(std::time::UNIX_EPOCH)
598            .map(|d| d.as_millis() as i64)
599            .unwrap_or(0);
600        lock.history.push(amalgamation::AmalgamationHistoryEntry {
601            amalgamation_id: pending.summary.amalgamation_id,
602            genome_title: pending.summary.genome_title,
603            circuit_size: pending.summary.circuit_size,
604            status: "cancelled".to_string(),
605            timestamp_ms: now_ms,
606        });
607
608        tracing::info!(
609            target: "feagi-api",
610            "🧬 [AMALGAMATION] Cancelled and cleared pending amalgamation id={}",
611            lock.history
612                .last()
613                .map(|e| e.amalgamation_id.clone())
614                .unwrap_or_else(|| "<unknown>".to_string())
615        );
616    }
617    Ok(Json(HashMap::from([(
618        "message".to_string(),
619        "Amalgamation cancelled".to_string(),
620    )])))
621}
622
623/// Append additional structures to the current genome.
624#[utoipa::path(post, path = "/v1/feagi/genome/append", tag = "genome")]
625pub async fn post_genome_append(
626    State(_state): State<ApiState>,
627    Json(_req): Json<HashMap<String, serde_json::Value>>,
628) -> ApiResult<Json<HashMap<String, String>>> {
629    Err(ApiError::internal("Not yet implemented"))
630}
631
632/// Load the minimal barebones genome with only essential neural structures.
633#[utoipa::path(
634    post,
635    path = "/v1/genome/upload/barebones",
636    responses(
637        (status = 200, description = "Barebones genome loaded successfully"),
638        (status = 500, description = "Failed to load genome")
639    ),
640    tag = "genome"
641)]
642pub async fn post_upload_barebones_genome(
643    State(state): State<ApiState>,
644) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
645    tracing::debug!(target: "feagi-api", "📥 POST /v1/genome/upload/barebones - Request received");
646    let result = load_default_genome(state, "barebones").await;
647    match &result {
648        Ok(_) => {
649            tracing::debug!(target: "feagi-api", "✅ POST /v1/genome/upload/barebones - Success")
650        }
651        Err(e) => {
652            tracing::error!(target: "feagi-api", "❌ POST /v1/genome/upload/barebones - Error: {:?}", e)
653        }
654    }
655    result
656}
657
658/// Load the essential genome with core sensory and motor areas.
659#[utoipa::path(
660    post,
661    path = "/v1/genome/upload/essential",
662    responses(
663        (status = 200, description = "Essential genome loaded successfully"),
664        (status = 500, description = "Failed to load genome")
665    ),
666    tag = "genome"
667)]
668pub async fn post_upload_essential_genome(
669    State(state): State<ApiState>,
670) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
671    load_default_genome(state, "essential").await
672}
673
674/// Helper function to load a default genome by name from embedded Rust genomes
675async fn load_default_genome(
676    state: ApiState,
677    genome_name: &str,
678) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
679    tracing::info!(target: "feagi-api", "🔄 Loading {} genome from embedded Rust genomes", genome_name);
680    tracing::debug!(target: "feagi-api", "   State components available: genome_service=true, runtime_service=true");
681    // Load genome from embedded Rust templates (no file I/O!)
682    let genome_json = match genome_name {
683        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON,
684        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON,
685        "test" => feagi_evolutionary::TEST_GENOME_JSON,
686        "vision" => feagi_evolutionary::VISION_GENOME_JSON,
687        _ => {
688            return Err(ApiError::invalid_input(format!(
689                "Unknown genome name '{}'. Available: barebones, essential, test, vision",
690                genome_name
691            )))
692        }
693    };
694
695    tracing::info!(target: "feagi-api","Using embedded {} genome ({} bytes), starting conversion...",
696                   genome_name, genome_json.len());
697
698    let params = LoadGenomeParams {
699        json_str: genome_json.to_string(),
700    };
701
702    tracing::info!(target: "feagi-api","Calling prioritized genome transition loader...");
703    let genome_info = load_genome_with_priority(&state, params, "default_genome_endpoint").await?;
704
705    tracing::info!(target: "feagi-api","Successfully loaded {} genome: {} cortical areas, {} brain regions",
706               genome_name, genome_info.cortical_area_count, genome_info.brain_region_count);
707
708    // Return response matching Python format
709    let mut response = HashMap::new();
710    response.insert("success".to_string(), serde_json::Value::Bool(true));
711    response.insert(
712        "message".to_string(),
713        serde_json::Value::String(format!("{} genome loaded successfully", genome_name)),
714    );
715    response.insert(
716        "cortical_area_count".to_string(),
717        serde_json::Value::Number(genome_info.cortical_area_count.into()),
718    );
719    response.insert(
720        "brain_region_count".to_string(),
721        serde_json::Value::Number(genome_info.brain_region_count.into()),
722    );
723    response.insert(
724        "genome_id".to_string(),
725        serde_json::Value::String(genome_info.genome_id),
726    );
727    response.insert(
728        "genome_title".to_string(),
729        serde_json::Value::String(genome_info.genome_title),
730    );
731
732    Ok(Json(response))
733}
734
735/// Get the current genome name.
736#[utoipa::path(
737    get,
738    path = "/v1/genome/name",
739    tag = "genome",
740    responses(
741        (status = 200, description = "Genome name", body = String)
742    )
743)]
744pub async fn get_name(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
745    // Get genome metadata to extract name
746    // TODO: Implement proper genome name retrieval from genome service
747    Ok(Json("default_genome".to_string()))
748}
749
750/// Get the genome creation or modification timestamp.
751#[utoipa::path(
752    get,
753    path = "/v1/genome/timestamp",
754    tag = "genome",
755    responses(
756        (status = 200, description = "Genome timestamp", body = i64)
757    )
758)]
759pub async fn get_timestamp(State(_state): State<ApiState>) -> ApiResult<Json<i64>> {
760    // TODO: Store and retrieve genome timestamp
761    Ok(Json(0))
762}
763
764/// Save the current genome to a file with optional ID and title parameters.
765#[utoipa::path(
766    post,
767    path = "/v1/genome/save",
768    tag = "genome",
769    responses(
770        (status = 200, description = "Genome saved", body = HashMap<String, String>)
771    )
772)]
773pub async fn post_save(
774    State(state): State<ApiState>,
775    Json(request): Json<HashMap<String, String>>,
776) -> ApiResult<Json<HashMap<String, String>>> {
777    use std::fs;
778    use std::path::Path;
779
780    info!("Saving genome to file");
781
782    // Get parameters
783    let genome_id = request.get("genome_id").cloned();
784    let genome_title = request.get("genome_title").cloned();
785    let file_path = request.get("file_path").cloned();
786
787    // Create save parameters
788    let params = feagi_services::SaveGenomeParams {
789        genome_id,
790        genome_title,
791    };
792
793    // Call genome service to generate JSON
794    let genome_service = state.genome_service.as_ref();
795    let genome_json = genome_service
796        .save_genome(params)
797        .await
798        .map_err(|e| ApiError::internal(format!("Failed to save genome: {}", e)))?;
799
800    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at save time.
801    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
802    let genome_value: serde_json::Value = serde_json::from_str(&genome_json)
803        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
804    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
805    let genome_json = serde_json::to_string_pretty(&genome_value)
806        .map_err(|e| ApiError::internal(format!("Failed to serialize genome JSON: {}", e)))?;
807
808    // Determine file path
809    let save_path = if let Some(path) = file_path {
810        std::path::PathBuf::from(path)
811    } else {
812        // Default to hidden genome directory with timestamp.
813        let timestamp = std::time::SystemTime::now()
814            .duration_since(std::time::UNIX_EPOCH)
815            .unwrap()
816            .as_secs();
817        std::path::PathBuf::from(".genome").join(format!("saved_genome_{}.json", timestamp))
818    };
819
820    // Ensure parent directory exists
821    if let Some(parent) = Path::new(&save_path).parent() {
822        fs::create_dir_all(parent)
823            .map_err(|e| ApiError::internal(format!("Failed to create directory: {}", e)))?;
824    }
825
826    // Write to file
827    fs::write(&save_path, genome_json)
828        .map_err(|e| ApiError::internal(format!("Failed to write file: {}", e)))?;
829
830    info!("✅ Genome saved successfully to: {}", save_path.display());
831
832    Ok(Json(HashMap::from([
833        (
834            "message".to_string(),
835            "Genome saved successfully".to_string(),
836        ),
837        ("file_path".to_string(), save_path.display().to_string()),
838    ])))
839}
840
841/// Load a genome from a file by name.
842#[utoipa::path(
843    post,
844    path = "/v1/genome/load",
845    tag = "genome",
846    responses(
847        (status = 200, description = "Genome loaded", body = HashMap<String, serde_json::Value>)
848    )
849)]
850pub async fn post_load(
851    State(state): State<ApiState>,
852    Json(request): Json<HashMap<String, String>>,
853) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
854    let genome_name = request
855        .get("genome_name")
856        .ok_or_else(|| ApiError::invalid_input("genome_name required"))?;
857
858    // Load genome from defaults
859    let params = feagi_services::LoadGenomeParams {
860        json_str: format!("{{\"genome_title\": \"{}\"}}", genome_name),
861    };
862
863    let genome_info = load_genome_with_priority(&state, params, "post_load").await?;
864
865    let mut response = HashMap::new();
866    response.insert(
867        "message".to_string(),
868        serde_json::json!("Genome loaded successfully"),
869    );
870    response.insert(
871        "genome_title".to_string(),
872        serde_json::json!(genome_info.genome_title),
873    );
874
875    Ok(Json(response))
876}
877
878/// Upload and load a genome from JSON payload.
879#[utoipa::path(
880    post,
881    path = "/v1/genome/upload",
882    tag = "genome",
883    responses(
884        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>)
885    )
886)]
887pub async fn post_upload(
888    State(state): State<ApiState>,
889    Json(genome_json): Json<serde_json::Value>,
890) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
891    // Convert to JSON string
892    let json_str = serde_json::to_string(&genome_json)
893        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
894
895    let params = LoadGenomeParams { json_str };
896    let genome_info = load_genome_with_priority(&state, params, "post_upload").await?;
897
898    let mut response = HashMap::new();
899    response.insert("success".to_string(), serde_json::json!(true));
900    response.insert(
901        "message".to_string(),
902        serde_json::json!("Genome uploaded successfully"),
903    );
904    response.insert(
905        "cortical_area_count".to_string(),
906        serde_json::json!(genome_info.cortical_area_count),
907    );
908    response.insert(
909        "brain_region_count".to_string(),
910        serde_json::json!(genome_info.brain_region_count),
911    );
912
913    Ok(Json(response))
914}
915
916/// Download the current genome as a JSON document.
917#[utoipa::path(
918    get,
919    path = "/v1/genome/download",
920    tag = "genome",
921    responses(
922        (status = 200, description = "Genome JSON", body = HashMap<String, serde_json::Value>)
923    )
924)]
925pub async fn get_download(State(state): State<ApiState>) -> ApiResult<Json<serde_json::Value>> {
926    info!("🦀 [API] GET /v1/genome/download - Downloading current genome");
927    let genome_service = state.genome_service.as_ref();
928
929    // Get genome as JSON string
930    let genome_json_str = genome_service
931        .save_genome(feagi_services::types::SaveGenomeParams {
932            genome_id: None,
933            genome_title: None,
934        })
935        .await
936        .map_err(|e| {
937            tracing::error!("Failed to export genome: {}", e);
938            ApiError::internal(format!("Failed to export genome: {}", e))
939        })?;
940
941    // Parse to Value for JSON response
942    let genome_value: serde_json::Value = serde_json::from_str(&genome_json_str)
943        .map_err(|e| ApiError::internal(format!("Failed to parse genome JSON: {}", e)))?;
944
945    // Ensure physiology.simulation_timestep reflects the *current* runtime timestep at download time.
946    let simulation_timestep_s = get_current_runtime_simulation_timestep_s(&state).await?;
947    let genome_value = inject_simulation_timestep_into_genome(genome_value, simulation_timestep_s)?;
948
949    info!(
950        "✅ Genome download complete, {} bytes",
951        genome_json_str.len()
952    );
953    Ok(Json(genome_value))
954}
955
956#[cfg(test)]
957mod tests {
958    use super::*;
959    use serde_json::json;
960
961    #[test]
962    fn test_inject_simulation_timestep_into_genome_updates_physio_key() {
963        let genome = json!({
964            "version": "3.0",
965            "physiology": {
966                "simulation_timestep": 0.025,
967                "max_age": 10000000
968            }
969        });
970
971        let updated = inject_simulation_timestep_into_genome(genome, 0.05).unwrap();
972        assert_eq!(updated["physiology"]["simulation_timestep"], json!(0.05));
973        assert_eq!(updated["physiology"]["max_age"], json!(10000000));
974    }
975
976    #[test]
977    fn test_inject_simulation_timestep_into_genome_errors_when_missing_physio() {
978        let genome = json!({ "version": "3.0" });
979        let err = inject_simulation_timestep_into_genome(genome, 0.05).unwrap_err();
980        assert!(format!("{err:?}").contains("physiology"));
981    }
982}
983
984/// Get genome properties including metadata, size, and configuration details.
985#[utoipa::path(
986    get,
987    path = "/v1/genome/properties",
988    tag = "genome",
989    responses(
990        (status = 200, description = "Genome properties", body = HashMap<String, serde_json::Value>)
991    )
992)]
993pub async fn get_properties(
994    State(_state): State<ApiState>,
995) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
996    // TODO: Implement proper metadata retrieval from genome service
997    Ok(Json(HashMap::new()))
998}
999
1000/// Validate a genome structure for correctness and completeness.
1001#[utoipa::path(
1002    post,
1003    path = "/v1/genome/validate",
1004    tag = "genome",
1005    responses(
1006        (status = 200, description = "Validation result", body = HashMap<String, serde_json::Value>)
1007    )
1008)]
1009pub async fn post_validate(
1010    State(_state): State<ApiState>,
1011    Json(_genome): Json<serde_json::Value>,
1012) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1013    // TODO: Implement genome validation
1014    let mut response = HashMap::new();
1015    response.insert("valid".to_string(), serde_json::json!(true));
1016    response.insert("errors".to_string(), serde_json::json!([]));
1017    response.insert("warnings".to_string(), serde_json::json!([]));
1018
1019    Ok(Json(response))
1020}
1021
1022/// Transform genome between different formats (flat to hierarchical or vice versa).
1023#[utoipa::path(
1024    post,
1025    path = "/v1/genome/transform",
1026    tag = "genome",
1027    responses(
1028        (status = 200, description = "Transformed genome", body = HashMap<String, serde_json::Value>)
1029    )
1030)]
1031pub async fn post_transform(
1032    State(_state): State<ApiState>,
1033    Json(_request): Json<HashMap<String, serde_json::Value>>,
1034) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1035    // TODO: Implement genome transformation
1036    let mut response = HashMap::new();
1037    response.insert(
1038        "message".to_string(),
1039        serde_json::json!("Genome transformation not yet implemented"),
1040    );
1041
1042    Ok(Json(response))
1043}
1044
1045/// Clone the current genome with a new name, creating an independent copy.
1046#[utoipa::path(
1047    post,
1048    path = "/v1/genome/clone",
1049    tag = "genome",
1050    responses(
1051        (status = 200, description = "Genome cloned", body = HashMap<String, String>)
1052    )
1053)]
1054pub async fn post_clone(
1055    State(_state): State<ApiState>,
1056    Json(_request): Json<HashMap<String, String>>,
1057) -> ApiResult<Json<HashMap<String, String>>> {
1058    // TODO: Implement genome cloning
1059    Ok(Json(HashMap::from([(
1060        "message".to_string(),
1061        "Genome cloning not yet implemented".to_string(),
1062    )])))
1063}
1064
1065/// Reset genome to its default state, clearing all cortical areas and brain regions.
1066/// Use before loading a new genome when "cortical area already exists" errors occur.
1067#[utoipa::path(
1068    post,
1069    path = "/v1/genome/reset",
1070    tag = "genome",
1071    responses(
1072        (status = 200, description = "Genome reset", body = HashMap<String, String>),
1073        (status = 409, description = "Genome transition in progress"),
1074        (status = 500, description = "Reset failed")
1075    )
1076)]
1077pub async fn post_reset(State(state): State<ApiState>) -> ApiResult<Json<HashMap<String, String>>> {
1078    let _lock = state.genome_transition_lock.try_lock().map_err(|_| {
1079        ApiError::conflict("Another genome transition is in progress; wait for it to finish")
1080    })?;
1081
1082    let genome_service = state.genome_service.as_ref();
1083    genome_service.reset_connectome().await.map_err(|e| {
1084        tracing::error!(target: "feagi-api", "Genome reset failed: {}", e);
1085        ApiError::internal(format!("Genome reset failed: {}", e))
1086    })?;
1087
1088    info!(target: "feagi-api", "Genome reset complete - connectome cleared");
1089    Ok(Json(HashMap::from([(
1090        "message".to_string(),
1091        "Genome reset complete. Connectome cleared. Load a new genome to continue.".to_string(),
1092    )])))
1093}
1094
1095/// Get genome metadata (alternative endpoint to properties).
1096#[utoipa::path(
1097    get,
1098    path = "/v1/genome/metadata",
1099    tag = "genome",
1100    responses(
1101        (status = 200, description = "Genome metadata", body = HashMap<String, serde_json::Value>)
1102    )
1103)]
1104pub async fn get_metadata(
1105    State(state): State<ApiState>,
1106) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1107    get_properties(State(state)).await
1108}
1109
1110/// Merge another genome into the current genome, combining their structures.
1111#[utoipa::path(
1112    post,
1113    path = "/v1/genome/merge",
1114    tag = "genome",
1115    responses(
1116        (status = 200, description = "Genome merged", body = HashMap<String, serde_json::Value>)
1117    )
1118)]
1119pub async fn post_merge(
1120    State(_state): State<ApiState>,
1121    Json(_request): Json<HashMap<String, serde_json::Value>>,
1122) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1123    // TODO: Implement genome merging
1124    let mut response = HashMap::new();
1125    response.insert(
1126        "message".to_string(),
1127        serde_json::json!("Genome merging not yet implemented"),
1128    );
1129
1130    Ok(Json(response))
1131}
1132
1133/// Get a diff comparison between two genomes showing their differences.
1134#[utoipa::path(
1135    get,
1136    path = "/v1/genome/diff",
1137    tag = "genome",
1138    params(
1139        ("genome_a" = String, Query, description = "First genome name"),
1140        ("genome_b" = String, Query, description = "Second genome name")
1141    ),
1142    responses(
1143        (status = 200, description = "Genome diff", body = HashMap<String, serde_json::Value>)
1144    )
1145)]
1146pub async fn get_diff(
1147    State(_state): State<ApiState>,
1148    Query(_params): Query<HashMap<String, String>>,
1149) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1150    // TODO: Implement genome diffing
1151    let mut response = HashMap::new();
1152    response.insert("differences".to_string(), serde_json::json!([]));
1153
1154    Ok(Json(response))
1155}
1156
1157/// Export genome in a specific format (JSON, YAML, binary, etc.).
1158#[utoipa::path(
1159    post,
1160    path = "/v1/genome/export_format",
1161    tag = "genome",
1162    responses(
1163        (status = 200, description = "Exported genome", body = HashMap<String, serde_json::Value>)
1164    )
1165)]
1166pub async fn post_export_format(
1167    State(_state): State<ApiState>,
1168    Json(_request): Json<HashMap<String, String>>,
1169) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1170    // TODO: Implement format-specific export
1171    let mut response = HashMap::new();
1172    response.insert(
1173        "message".to_string(),
1174        serde_json::json!("Format export not yet implemented"),
1175    );
1176
1177    Ok(Json(response))
1178}
1179
1180// EXACT Python paths:
1181/// Get current amalgamation status and configuration.
1182#[utoipa::path(get, path = "/v1/genome/amalgamation", tag = "genome")]
1183pub async fn get_amalgamation(
1184    State(state): State<ApiState>,
1185) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1186    let lock = state.amalgamation_state.read();
1187    let mut response = HashMap::new();
1188    if let Some(p) = lock.pending.as_ref() {
1189        response.insert(
1190            "pending".to_string(),
1191            amalgamation::pending_summary_to_health_json(&p.summary),
1192        );
1193    } else {
1194        response.insert("pending".to_string(), serde_json::Value::Null);
1195    }
1196    Ok(Json(response))
1197}
1198
1199/// Get history of all genome amalgamation operations performed.
1200#[utoipa::path(get, path = "/v1/genome/amalgamation_history", tag = "genome")]
1201pub async fn get_amalgamation_history_exact(
1202    State(state): State<ApiState>,
1203) -> ApiResult<Json<Vec<HashMap<String, serde_json::Value>>>> {
1204    let lock = state.amalgamation_state.read();
1205    let mut out: Vec<HashMap<String, serde_json::Value>> = Vec::new();
1206    for entry in &lock.history {
1207        out.push(HashMap::from([
1208            (
1209                "amalgamation_id".to_string(),
1210                serde_json::json!(entry.amalgamation_id),
1211            ),
1212            (
1213                "genome_title".to_string(),
1214                serde_json::json!(entry.genome_title),
1215            ),
1216            (
1217                "circuit_size".to_string(),
1218                serde_json::json!(entry.circuit_size),
1219            ),
1220            ("status".to_string(), serde_json::json!(entry.status)),
1221            (
1222                "timestamp_ms".to_string(),
1223                serde_json::json!(entry.timestamp_ms),
1224            ),
1225        ]));
1226    }
1227    Ok(Json(out))
1228}
1229
1230/// Get metadata about all available cortical types including supported encodings and configurations.
1231#[utoipa::path(get, path = "/v1/genome/cortical_template", tag = "genome")]
1232pub async fn get_cortical_template(
1233    State(_state): State<ApiState>,
1234) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1235    use feagi_structures::genomic::cortical_area::io_cortical_area_configuration_flag::{
1236        FrameChangeHandling, IOCorticalAreaConfigurationFlag, PercentageNeuronPositioning,
1237    };
1238    use feagi_structures::genomic::{MotorCorticalUnit, SensoryCorticalUnit};
1239    use serde_json::json;
1240
1241    let mut templates = HashMap::new();
1242
1243    // Helper to convert data type to human-readable format.
1244    //
1245    // NOTE: This endpoint is designed for tool/UIs (e.g. BV) and must be
1246    // deterministic across platforms and runs. No fallbacks.
1247    let data_type_to_json = |dt: IOCorticalAreaConfigurationFlag| -> serde_json::Value {
1248        let (variant, frame, positioning) = match dt {
1249            IOCorticalAreaConfigurationFlag::Boolean => {
1250                ("Boolean", FrameChangeHandling::Absolute, None)
1251            }
1252            IOCorticalAreaConfigurationFlag::Percentage(f, p) => ("Percentage", f, Some(p)),
1253            IOCorticalAreaConfigurationFlag::Percentage2D(f, p) => ("Percentage2D", f, Some(p)),
1254            IOCorticalAreaConfigurationFlag::Percentage3D(f, p) => ("Percentage3D", f, Some(p)),
1255            IOCorticalAreaConfigurationFlag::Percentage4D(f, p) => ("Percentage4D", f, Some(p)),
1256            IOCorticalAreaConfigurationFlag::SignedPercentage(f, p) => {
1257                ("SignedPercentage", f, Some(p))
1258            }
1259            IOCorticalAreaConfigurationFlag::SignedPercentage2D(f, p) => {
1260                ("SignedPercentage2D", f, Some(p))
1261            }
1262            IOCorticalAreaConfigurationFlag::SignedPercentage3D(f, p) => {
1263                ("SignedPercentage3D", f, Some(p))
1264            }
1265            IOCorticalAreaConfigurationFlag::SignedPercentage4D(f, p) => {
1266                ("SignedPercentage4D", f, Some(p))
1267            }
1268            IOCorticalAreaConfigurationFlag::CartesianPlane(f) => ("CartesianPlane", f, None),
1269            IOCorticalAreaConfigurationFlag::Misc(f) => ("Misc", f, None),
1270        };
1271
1272        let frame_str = match frame {
1273            FrameChangeHandling::Absolute => "Absolute",
1274            FrameChangeHandling::Incremental => "Incremental",
1275        };
1276
1277        let positioning_str = positioning.map(|p| match p {
1278            PercentageNeuronPositioning::Linear => "Linear",
1279            PercentageNeuronPositioning::Fractional => "Fractional",
1280        });
1281
1282        json!({
1283            "variant": variant,
1284            "frame_change_handling": frame_str,
1285            "percentage_positioning": positioning_str,
1286            "config_value": dt.to_data_type_configuration_flag()
1287        })
1288    };
1289
1290    // Add motor types
1291    for motor_unit in MotorCorticalUnit::list_all() {
1292        let friendly_name = motor_unit.get_friendly_name();
1293        let cortical_id_ref = motor_unit.get_cortical_id_unit_reference();
1294        let num_areas = motor_unit.get_number_cortical_areas();
1295        let topology = motor_unit.get_unit_default_topology();
1296
1297        // BREAKING CHANGE (unreleased API):
1298        // - Remove unit-level `supported_data_types`.
1299        // - Expose per-subunit metadata, because some units (e.g. Gaze) have heterogeneous subunits
1300        //   with different IOCorticalAreaConfigurationFlag variants (Percentage2D vs Percentage).
1301        //
1302        // We derive supported types by:
1303        // - generating canonical cortical IDs from the MotorCorticalUnit template for each
1304        //   (frame_change_handling, percentage_neuron_positioning) combination
1305        // - extracting the IO configuration flag from each cortical ID
1306        // - grouping supported_data_types per subunit index
1307        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1308        use serde_json::{Map, Value};
1309        use std::collections::HashMap as StdHashMap;
1310
1311        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1312
1313        // Initialize subunits with topology-derived properties.
1314        for (sub_idx, topo) in topology {
1315            subunits.insert(
1316                sub_idx.get().to_string(),
1317                json!({
1318                    "relative_position": topo.relative_position,
1319                    "channel_dimensions_default": topo.channel_dimensions_default,
1320                    "channel_dimensions_min": topo.channel_dimensions_min,
1321                    "channel_dimensions_max": topo.channel_dimensions_max,
1322                    "supported_data_types": Vec::<serde_json::Value>::new(),
1323                }),
1324            );
1325        }
1326
1327        // Build per-subunit supported_data_types (deduped).
1328        let allowed_frames = motor_unit.get_allowed_frame_change_handling();
1329        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1330            Some(allowed) => allowed.to_vec(),
1331            None => vec![
1332                FrameChangeHandling::Absolute,
1333                FrameChangeHandling::Incremental,
1334            ],
1335        };
1336
1337        let positionings = [
1338            PercentageNeuronPositioning::Linear,
1339            PercentageNeuronPositioning::Fractional,
1340        ];
1341
1342        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1343            StdHashMap::new();
1344
1345        for frame in frames {
1346            for positioning in positionings {
1347                let mut map: Map<String, Value> = Map::new();
1348                map.insert(
1349                    "frame_change_handling".to_string(),
1350                    serde_json::to_value(frame).unwrap_or(Value::Null),
1351                );
1352                map.insert(
1353                    "percentage_neuron_positioning".to_string(),
1354                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1355                );
1356
1357                // Use unit index 0 for template enumeration (index does not affect IO flags).
1358                let cortical_ids = motor_unit
1359                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1360                        CorticalUnitIndex::from(0u8),
1361                        map,
1362                    );
1363
1364                if let Ok(ids) = cortical_ids {
1365                    for (i, id) in ids.into_iter().enumerate() {
1366                        if let Ok(flag) = id.extract_io_data_flag() {
1367                            let dt_json = data_type_to_json(flag);
1368                            let subunit_key = i.to_string();
1369
1370                            let dedup_key = format!(
1371                                "{}|{}|{}",
1372                                dt_json
1373                                    .get("variant")
1374                                    .and_then(|v| v.as_str())
1375                                    .unwrap_or(""),
1376                                dt_json
1377                                    .get("frame_change_handling")
1378                                    .and_then(|v| v.as_str())
1379                                    .unwrap_or(""),
1380                                dt_json
1381                                    .get("percentage_positioning")
1382                                    .and_then(|v| v.as_str())
1383                                    .unwrap_or("")
1384                            );
1385
1386                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1387                            if !seen.insert(dedup_key) {
1388                                continue;
1389                            }
1390
1391                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1392                                if let Some(arr) = subunit_obj
1393                                    .get_mut("supported_data_types")
1394                                    .and_then(|v| v.as_array_mut())
1395                                {
1396                                    arr.push(dt_json);
1397                                }
1398                            }
1399                        }
1400                    }
1401                }
1402            }
1403        }
1404
1405        templates.insert(
1406            format!("o{}", String::from_utf8_lossy(&cortical_id_ref)),
1407            json!({
1408                "type": "motor",
1409                "friendly_name": friendly_name,
1410                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1411                "number_of_cortical_areas": num_areas,
1412                "subunits": subunits,
1413                "description": format!("Motor output: {}", friendly_name)
1414            }),
1415        );
1416    }
1417
1418    // Add sensory types
1419    for sensory_unit in SensoryCorticalUnit::list_all() {
1420        let friendly_name = sensory_unit.get_friendly_name();
1421        let cortical_id_ref = sensory_unit.get_cortical_id_unit_reference();
1422        let num_areas = sensory_unit.get_number_cortical_areas();
1423        let topology = sensory_unit.get_unit_default_topology();
1424
1425        use feagi_structures::genomic::cortical_area::descriptors::CorticalUnitIndex;
1426        use serde_json::{Map, Value};
1427        use std::collections::HashMap as StdHashMap;
1428
1429        let mut subunits: StdHashMap<String, serde_json::Value> = StdHashMap::new();
1430
1431        for (sub_idx, topo) in topology {
1432            subunits.insert(
1433                sub_idx.get().to_string(),
1434                json!({
1435                    "relative_position": topo.relative_position,
1436                    "channel_dimensions_default": topo.channel_dimensions_default,
1437                    "channel_dimensions_min": topo.channel_dimensions_min,
1438                    "channel_dimensions_max": topo.channel_dimensions_max,
1439                    "supported_data_types": Vec::<serde_json::Value>::new(),
1440                }),
1441            );
1442        }
1443
1444        let allowed_frames = sensory_unit.get_allowed_frame_change_handling();
1445        let frames: Vec<FrameChangeHandling> = match allowed_frames {
1446            Some(allowed) => allowed.to_vec(),
1447            None => vec![
1448                FrameChangeHandling::Absolute,
1449                FrameChangeHandling::Incremental,
1450            ],
1451        };
1452
1453        let positionings = [
1454            PercentageNeuronPositioning::Linear,
1455            PercentageNeuronPositioning::Fractional,
1456        ];
1457
1458        let mut per_subunit_dedup: StdHashMap<String, std::collections::HashSet<String>> =
1459            StdHashMap::new();
1460
1461        for frame in frames {
1462            for positioning in positionings {
1463                let mut map: Map<String, Value> = Map::new();
1464                map.insert(
1465                    "frame_change_handling".to_string(),
1466                    serde_json::to_value(frame).unwrap_or(Value::Null),
1467                );
1468                map.insert(
1469                    "percentage_neuron_positioning".to_string(),
1470                    serde_json::to_value(positioning).unwrap_or(Value::Null),
1471                );
1472
1473                let cortical_ids = sensory_unit
1474                    .get_cortical_id_vector_from_index_and_serde_io_configuration_flags(
1475                        CorticalUnitIndex::from(0u8),
1476                        map,
1477                    );
1478
1479                if let Ok(ids) = cortical_ids {
1480                    for (i, id) in ids.into_iter().enumerate() {
1481                        if let Ok(flag) = id.extract_io_data_flag() {
1482                            let dt_json = data_type_to_json(flag);
1483                            let subunit_key = i.to_string();
1484
1485                            let dedup_key = format!(
1486                                "{}|{}|{}",
1487                                dt_json
1488                                    .get("variant")
1489                                    .and_then(|v| v.as_str())
1490                                    .unwrap_or(""),
1491                                dt_json
1492                                    .get("frame_change_handling")
1493                                    .and_then(|v| v.as_str())
1494                                    .unwrap_or(""),
1495                                dt_json
1496                                    .get("percentage_positioning")
1497                                    .and_then(|v| v.as_str())
1498                                    .unwrap_or("")
1499                            );
1500
1501                            let seen = per_subunit_dedup.entry(subunit_key.clone()).or_default();
1502                            if !seen.insert(dedup_key) {
1503                                continue;
1504                            }
1505
1506                            if let Some(subunit_obj) = subunits.get_mut(&subunit_key) {
1507                                if let Some(arr) = subunit_obj
1508                                    .get_mut("supported_data_types")
1509                                    .and_then(|v| v.as_array_mut())
1510                                {
1511                                    arr.push(dt_json);
1512                                }
1513                            }
1514                        }
1515                    }
1516                }
1517            }
1518        }
1519
1520        templates.insert(
1521            format!("i{}", String::from_utf8_lossy(&cortical_id_ref)),
1522            json!({
1523                "type": "sensory",
1524                "friendly_name": friendly_name,
1525                "cortical_id_prefix": String::from_utf8_lossy(&cortical_id_ref).to_string(),
1526                "number_of_cortical_areas": num_areas,
1527                "subunits": subunits,
1528                "description": format!("Sensory input: {}", friendly_name)
1529            }),
1530        );
1531    }
1532
1533    Ok(Json(templates))
1534}
1535
1536/// Get list of available embedded default genome templates (barebones, essential, test, vision).
1537#[utoipa::path(get, path = "/v1/genome/defaults/files", tag = "genome")]
1538pub async fn get_defaults_files(State(_state): State<ApiState>) -> ApiResult<Json<Vec<String>>> {
1539    Ok(Json(vec![
1540        "barebones".to_string(),
1541        "essential".to_string(),
1542        "test".to_string(),
1543        "vision".to_string(),
1544    ]))
1545}
1546
1547/// Download a specific brain region from the genome.
1548#[utoipa::path(get, path = "/v1/genome/download_region", tag = "genome")]
1549pub async fn get_download_region(
1550    State(state): State<ApiState>,
1551    Query(params): Query<HashMap<String, String>>,
1552) -> ApiResult<Json<serde_json::Value>> {
1553    let region_id = params
1554        .get("region_id")
1555        .cloned()
1556        .ok_or_else(|| ApiError::invalid_input("region_id query parameter is required"))?;
1557    let json_str = state
1558        .genome_service
1559        .export_region_genome(region_id)
1560        .await
1561        .map_err(ApiError::from)?;
1562    let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
1563        ApiError::internal(format!("Exported region genome JSON is invalid: {}", e))
1564    })?;
1565    Ok(Json(value))
1566}
1567
1568/// Get the current genome number or generation identifier.
1569#[utoipa::path(get, path = "/v1/genome/genome_number", tag = "genome")]
1570pub async fn get_genome_number(State(_state): State<ApiState>) -> ApiResult<Json<i32>> {
1571    Ok(Json(0))
1572}
1573
1574/// Perform genome amalgamation by specifying a filename.
1575#[utoipa::path(post, path = "/v1/genome/amalgamation_by_filename", tag = "genome")]
1576pub async fn post_amalgamation_by_filename(
1577    State(state): State<ApiState>,
1578    Json(req): Json<HashMap<String, String>>,
1579) -> ApiResult<Json<HashMap<String, String>>> {
1580    // Deterministic implementation:
1581    // - Supports embedded Rust template genomes by name (no filesystem I/O).
1582    // - For all other filenames, require /amalgamation_by_payload.
1583    let file_name = req
1584        .get("file_name")
1585        .or_else(|| req.get("filename"))
1586        .or_else(|| req.get("genome_file_name"))
1587        .ok_or_else(|| ApiError::invalid_input("file_name required"))?;
1588
1589    let genome_json = match file_name.as_str() {
1590        "barebones" => feagi_evolutionary::BAREBONES_GENOME_JSON.to_string(),
1591        "essential" => feagi_evolutionary::ESSENTIAL_GENOME_JSON.to_string(),
1592        "test" => feagi_evolutionary::TEST_GENOME_JSON.to_string(),
1593        "vision" => feagi_evolutionary::VISION_GENOME_JSON.to_string(),
1594        other => {
1595            return Err(ApiError::invalid_input(format!(
1596                "Unsupported file_name '{}'. Use /v1/genome/amalgamation_by_payload for arbitrary genomes.",
1597                other
1598            )))
1599        }
1600    };
1601
1602    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, genome_json)?;
1603
1604    Ok(Json(HashMap::from([
1605        ("message".to_string(), "Amalgamation queued".to_string()),
1606        ("amalgamation_id".to_string(), amalgamation_id),
1607    ])))
1608}
1609
1610/// Perform genome amalgamation using a direct JSON payload.
1611#[utoipa::path(post, path = "/v1/genome/amalgamation_by_payload", tag = "genome")]
1612pub async fn post_amalgamation_by_payload(
1613    State(state): State<ApiState>,
1614    Json(req): Json<serde_json::Value>,
1615) -> ApiResult<Json<HashMap<String, String>>> {
1616    let json_str = serde_json::to_string(&req)
1617        .map_err(|e| ApiError::invalid_input(format!("Invalid JSON: {}", e)))?;
1618    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1619
1620    Ok(Json(HashMap::from([
1621        ("message".to_string(), "Amalgamation queued".to_string()),
1622        ("amalgamation_id".to_string(), amalgamation_id),
1623    ])))
1624}
1625
1626/// Perform genome amalgamation by uploading a genome file.
1627#[cfg(feature = "http")]
1628#[utoipa::path(
1629    post,
1630    path = "/v1/genome/amalgamation_by_upload",
1631    tag = "genome",
1632    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1633    responses(
1634        (status = 200, description = "Amalgamation queued", body = HashMap<String, String>),
1635        (status = 400, description = "Invalid request"),
1636        (status = 500, description = "Internal server error")
1637    )
1638)]
1639pub async fn post_amalgamation_by_upload(
1640    State(state): State<ApiState>,
1641    mut multipart: Multipart,
1642) -> ApiResult<Json<HashMap<String, String>>> {
1643    let mut genome_json: Option<String> = None;
1644
1645    while let Some(field) = multipart
1646        .next_field()
1647        .await
1648        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1649    {
1650        if field.name() == Some("file") {
1651            let bytes = field.bytes().await.map_err(|e| {
1652                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1653            })?;
1654
1655            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1656                ApiError::invalid_input(format!(
1657                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1658                    e
1659                ))
1660            })?;
1661            genome_json = Some(json_str.to_string());
1662            break;
1663        }
1664    }
1665
1666    let json_str =
1667        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1668    let amalgamation_id = queue_amalgamation_from_genome_json_str(&state, json_str)?;
1669
1670    Ok(Json(HashMap::from([
1671        ("message".to_string(), "Amalgamation queued".to_string()),
1672        ("amalgamation_id".to_string(), amalgamation_id),
1673    ])))
1674}
1675
1676/// Append structures to the genome from a file.
1677#[cfg(feature = "http")]
1678#[utoipa::path(
1679    post,
1680    path = "/v1/genome/append-file",
1681    tag = "genome",
1682    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1683    responses(
1684        (status = 200, description = "Append processed", body = HashMap<String, String>)
1685    )
1686)]
1687pub async fn post_append_file(
1688    State(_state): State<ApiState>,
1689    mut _multipart: Multipart,
1690) -> ApiResult<Json<HashMap<String, String>>> {
1691    Ok(Json(HashMap::from([(
1692        "message".to_string(),
1693        "Not yet implemented".to_string(),
1694    )])))
1695}
1696
1697/// Upload and load a genome from a file.
1698#[cfg(feature = "http")]
1699#[utoipa::path(
1700    post,
1701    path = "/v1/genome/upload/file",
1702    tag = "genome",
1703    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1704    responses(
1705        (status = 200, description = "Genome uploaded", body = HashMap<String, serde_json::Value>),
1706        (status = 400, description = "Invalid request"),
1707        (status = 500, description = "Internal server error")
1708    )
1709)]
1710pub async fn post_upload_file(
1711    State(state): State<ApiState>,
1712    mut multipart: Multipart,
1713) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
1714    let mut genome_json: Option<String> = None;
1715
1716    while let Some(field) = multipart
1717        .next_field()
1718        .await
1719        .map_err(|e| ApiError::invalid_input(format!("Invalid multipart upload: {}", e)))?
1720    {
1721        if field.name() == Some("file") {
1722            let bytes = field.bytes().await.map_err(|e| {
1723                ApiError::invalid_input(format!("Failed to read uploaded file: {}", e))
1724            })?;
1725
1726            let json_str = std::str::from_utf8(&bytes).map_err(|e| {
1727                ApiError::invalid_input(format!(
1728                    "Uploaded file must be UTF-8 encoded JSON (decode error: {})",
1729                    e
1730                ))
1731            })?;
1732            genome_json = Some(json_str.to_string());
1733            break;
1734        }
1735    }
1736
1737    let json_str =
1738        genome_json.ok_or_else(|| ApiError::invalid_input("Missing multipart field 'file'"))?;
1739
1740    let genome_info =
1741        load_genome_with_priority(&state, LoadGenomeParams { json_str }, "post_upload_file")
1742            .await?;
1743
1744    let mut response = HashMap::new();
1745    response.insert("success".to_string(), serde_json::json!(true));
1746    response.insert(
1747        "message".to_string(),
1748        serde_json::json!("Genome uploaded successfully"),
1749    );
1750    response.insert(
1751        "cortical_area_count".to_string(),
1752        serde_json::json!(genome_info.cortical_area_count),
1753    );
1754    response.insert(
1755        "brain_region_count".to_string(),
1756        serde_json::json!(genome_info.brain_region_count),
1757    );
1758
1759    Ok(Json(response))
1760}
1761
1762/// Upload a genome file with edit mode enabled.
1763#[cfg(feature = "http")]
1764#[utoipa::path(
1765    post,
1766    path = "/v1/genome/upload/file/edit",
1767    tag = "genome",
1768    request_body(content = GenomeFileUploadForm, content_type = "multipart/form-data"),
1769    responses(
1770        (status = 200, description = "Upload processed", body = HashMap<String, String>)
1771    )
1772)]
1773pub async fn post_upload_file_edit(
1774    State(_state): State<ApiState>,
1775    mut _multipart: Multipart,
1776) -> ApiResult<Json<HashMap<String, String>>> {
1777    Ok(Json(HashMap::from([(
1778        "message".to_string(),
1779        "Not yet implemented".to_string(),
1780    )])))
1781}
1782
1783/// Upload and load a genome from a JSON string.
1784#[utoipa::path(post, path = "/v1/genome/upload/string", tag = "genome")]
1785pub async fn post_upload_string(
1786    State(_state): State<ApiState>,
1787    Json(_req): Json<String>,
1788) -> ApiResult<Json<HashMap<String, String>>> {
1789    Ok(Json(HashMap::from([(
1790        "message".to_string(),
1791        "Not yet implemented".to_string(),
1792    )])))
1793}