soma-core 2.0.1

World's first production-ready self-aware development system with meta-cognitive capabilities and cognitive reasoning engine for intelligent development platforms
Documentation
// feedback_loop.rs: Multi-agent symbolic feedback and merge system for soma-core
// All comments in English (US) per coding_guidelines.md

use crate::agents::claude_agent::ClaudeAgent;
use crate::agents::gemini_agent::GeminiAgent;
use crate::agents::gpt4_agent::GPT4Agent;
use crate::agents::gpt4_agent::{CognitiveAgent, ProposedEdit};
use crate::dag::Node;
use crate::memory::SymbolicContext;
use std::collections::HashMap;
use std::fs;

/// Loads the current code context (e.g., from a file)
#[allow(dead_code)]
pub fn load_code_context(file: &str) -> Vec<String> {
    fs::read_to_string(file)
        .map(|s| s.lines().map(|l| l.to_string()).collect())
        .unwrap_or_default()
}

/// Creates a backup of a file before editing
#[allow(dead_code)]
pub fn backup_file(file_path: &str) -> Result<String, std::io::Error> {
    let backup_path = format!("{}.backup", file_path);
    fs::copy(file_path, &backup_path)?;
    Ok(backup_path)
}

/// Restores a file from its backup
#[allow(dead_code)]
pub fn restore_file(file_path: &str, backup_path: &str) -> Result<(), std::io::Error> {
    fs::copy(backup_path, file_path)?;
    fs::remove_file(backup_path)?;
    Ok(())
}

/// Applies a proposed edit to a file by replacing the specified line range
#[allow(dead_code)]
pub fn apply_edit_to_file(
    file_path: &str,
    edit: &ProposedEdit,
) -> Result<(), Box<dyn std::error::Error>> {
    let lines = load_code_context(file_path);
    let mut new_lines = lines.clone();

    // Validate line range
    if edit.line_range.0 == 0 || edit.line_range.1 > lines.len() {
        return Err(format!(
            "Invalid line range {:?} for file with {} lines",
            edit.line_range,
            lines.len()
        )
        .into());
    }

    // Convert 1-based line numbers to 0-based indices
    let start_idx = edit.line_range.0 - 1;
    let end_idx = edit.line_range.1.min(lines.len());

    // Replace the lines
    let edit_lines: Vec<String> = edit.new_code.lines().map(|s| s.to_string()).collect();
    new_lines.splice(start_idx..end_idx, edit_lines);

    // Write back to file
    let content = new_lines.join("\n");
    fs::write(file_path, content)?;

    println!("✅ Applied edit to {}: {}", file_path, edit.reason);
    Ok(())
}

/// Applies all consensus edits to their respective files with backup/restore
#[allow(dead_code)]
pub fn apply_consensus_edits(
    edits: &[&ProposedEdit],
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let mut backups = Vec::new();

    // Group edits by file
    let mut edits_by_file: HashMap<String, Vec<&ProposedEdit>> = HashMap::new();
    for edit in edits {
        edits_by_file
            .entry(edit.file.clone())
            .or_default()
            .push(edit);
    }

    for (file_path, file_edits) in edits_by_file {
        // Create backup
        if std::path::Path::new(&file_path).exists() {
            let backup_path = backup_file(&file_path)?;
            backups.push((file_path.clone(), backup_path));

            // Apply edits in reverse order (from bottom to top) to maintain line numbers
            let mut sorted_edits = file_edits;
            sorted_edits.sort_by(|a, b| b.line_range.0.cmp(&a.line_range.0));

            for edit in sorted_edits {
                apply_edit_to_file(&file_path, edit)?;
            }
        } else {
            println!("⚠️  File {} does not exist, skipping edit", file_path);
        }
    }

    Ok(backups.into_iter().map(|(_, backup)| backup).collect())
}

/// Shows the diff between original and edited files
#[allow(dead_code)]
pub fn show_file_diff(
    file_path: &str,
    backup_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let original = fs::read_to_string(backup_path)?;
    let edited = fs::read_to_string(file_path)?;

    println!("\n📄 File: {}", file_path);
    println!("{}", "=".repeat(60));

    let original_lines: Vec<&str> = original.lines().collect();
    let edited_lines: Vec<&str> = edited.lines().collect();

    let max_lines = original_lines.len().max(edited_lines.len());

    for i in 0..max_lines {
        let orig_line = original_lines.get(i).unwrap_or(&"");
        let edit_line = edited_lines.get(i).unwrap_or(&"");

        if orig_line != edit_line {
            if !orig_line.is_empty() {
                println!("- {}: {}", i + 1, orig_line);
            }
            if !edit_line.is_empty() {
                println!("+ {}: {}", i + 1, edit_line);
            }
        } else if !orig_line.is_empty() {
            println!("  {}: {}", i + 1, orig_line);
        }

        // Limit output for readability
        if i > 20 {
            println!("  ... ({} more lines)", max_lines - i - 1);
            break;
        }
    }

    Ok(())
}

/// Merge logic: Accepts edits if at least 2 agents agree on the same file/line range
pub fn merge_edits(edits: &[ProposedEdit]) -> Vec<&ProposedEdit> {
    let mut votes: HashMap<(String, (usize, usize)), Vec<&ProposedEdit>> = HashMap::new();
    for edit in edits {
        votes
            .entry((edit.file.clone(), edit.line_range))
            .or_default()
            .push(edit);
    }
    votes
        .values()
        .filter(|v| v.len() >= 2)
        .map(|v| v[0])
        .collect()
}

/// Coordination function: runs the feedback loop and logs meta information
pub fn run_feedback_loop(ctx: &SymbolicContext, _dag: &[Node]) {
    let agents: Vec<Box<dyn CognitiveAgent>> = vec![
        Box::new(GPT4Agent),
        Box::new(ClaudeAgent),
        Box::new(GeminiAgent),
    ];
    let mut all_edits = vec![];
    for agent in &agents {
        let edit = agent.propose_edit(ctx);
        all_edits.push(edit);
    }
    let merged = merge_edits(&all_edits);
    // Log all proposals and merged edits
    let meta_log = serde_json::json!({
        "timestamp": chrono::Utc::now().to_rfc3339(),
        "proposals": all_edits.iter().map(|e| serde_json::json!({
            "file": e.file,
            "line_range": e.line_range,
            "new_code": e.new_code,
            "reason": e.reason,
            "confidence": e.confidence,
        })).collect::<Vec<_>>(),
        "merged": merged.iter().map(|e| serde_json::json!({
            "file": e.file,
            "line_range": e.line_range,
            "new_code": e.new_code,
            "reason": e.reason,
            "confidence": e.confidence,
        })).collect::<Vec<_>>()
    });
    fs::write(
        "meta_log.json",
        serde_json::to_string_pretty(&meta_log).unwrap(),
    )
    .unwrap();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agents::gpt4_agent::ProposedEdit;
    use crate::memory::SymbolicContext;

    #[test]
    fn test_merge_edits_consensus() {
        let edits = vec![
            ProposedEdit {
                file: "test.rs".to_string(),
                line_range: (10, 12),
                new_code: "// Improved code".to_string(),
                reason: "Better readability".to_string(),
                confidence: 0.95,
            },
            ProposedEdit {
                file: "test.rs".to_string(),
                line_range: (10, 12),
                new_code: "// Enhanced code".to_string(),
                reason: "Better performance".to_string(),
                confidence: 0.90,
            },
            ProposedEdit {
                file: "other.rs".to_string(),
                line_range: (5, 7),
                new_code: "// Solo edit".to_string(),
                reason: "Isolated change".to_string(),
                confidence: 0.85,
            },
        ];

        let merged = merge_edits(&edits);
        assert_eq!(merged.len(), 1); // Only one edit should be merged (2+ consensus)
        assert_eq!(merged[0].file, "test.rs");
        assert_eq!(merged[0].line_range, (10, 12));
    }

    #[test]
    fn test_feedback_loop_execution() {
        let mut ctx = SymbolicContext::new();
        ctx.set("current_task", "improve code quality");

        let nodes = vec![];

        // This should create a meta_log.json file
        run_feedback_loop(&ctx, &nodes);

        // Verify the log file was created
        assert!(std::path::Path::new("meta_log.json").exists());

        // Read and verify the log content
        let log_content = std::fs::read_to_string("meta_log.json").unwrap();
        let log: serde_json::Value = serde_json::from_str(&log_content).unwrap();

        assert!(log["proposals"].is_array());
        assert_eq!(log["proposals"].as_array().unwrap().len(), 3); // 3 agents
        assert!(log["merged"].is_array());

        // Clean up
        std::fs::remove_file("meta_log.json").ok();
    }
}