spool-memory 0.1.0

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
use crate::domain::{ContextBundle, TargetTool};

pub fn render(bundle: &ContextBundle, max_chars: usize) -> String {
    let mut output = String::new();
    output.push_str(match bundle.input.target {
        TargetTool::Claude => "以下是给 Claude 使用的精简上下文。\n\n",
        TargetTool::Codex => "以下是给 Codex 使用的精简上下文。\n\n",
        TargetTool::Opencode => "以下是给 OpenCode 使用的精简上下文。\n\n",
    });

    if let Some(project) = &bundle.route.project {
        output.push_str(&format!("项目:{} ({})\n", project.name, project.id));
        output.push_str(&format!("项目命中原因:{}\n\n", project.reason));
    }

    if !bundle.route.modules.is_empty() {
        output.push_str("模块:\n");
        for module in &bundle.route.modules {
            output.push_str(&format!("- {}\n", module.id));
        }
        output.push('\n');
    }

    if !bundle.route.scenes.is_empty() {
        output.push_str("场景:\n");
        for scene in &bundle.route.scenes {
            output.push_str(&format!("- {}\n", scene.id));
        }
        output.push('\n');
    }

    if !bundle.route.lifecycle_candidates.is_empty() {
        output.push_str("记忆(accepted / canonical):\n");
        for candidate in &bundle.route.lifecycle_candidates {
            let summary = truncate_chars(&candidate.summary, 120);
            output.push_str(&format!(
                "- [{}] [{}] {}{}\n",
                candidate.score, candidate.memory_type, candidate.title, summary
            ));
        }
        output.push('\n');
    }

    output.push_str("候选笔记:\n");
    for candidate in &bundle.route.candidates {
        output.push_str(&format!(
            "- [{}] {}\n  {}\n",
            candidate.score, candidate.relative_path, candidate.excerpt
        ));
    }

    if output.chars().count() > max_chars {
        output.chars().take(max_chars).collect()
    } else {
        output
    }
}

fn truncate_chars(value: &str, max_chars: usize) -> String {
    let chars: Vec<char> = value.chars().collect();
    if chars.len() <= max_chars {
        return value.to_string();
    }
    let mut out: String = chars.iter().take(max_chars).collect();
    out.push('');
    out
}