lean-ctx 3.1.5

Context Runtime for AI Agents with CCP. 42 MCP tools, 10 read modes, 90+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing + diaries, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24 AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use std::path::Path;

use crate::core::protocol;
use crate::core::signatures::{extract_signatures, Signature};
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;

pub fn handle(path: &str, kind_filter: Option<&str>) -> (String, usize) {
    let content = match std::fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return (format!("ERROR: Cannot read {path}: {e}"), 0),
    };

    let ext = Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("");

    let sigs = extract_signatures(&content, ext);

    if sigs.is_empty() {
        return (format!("No symbols found in {path}"), 0);
    }

    let filtered = filter_by_kind(&sigs, kind_filter);

    let crp = CrpMode::from_env();
    let outline = format_outline(&filtered, path, crp);

    let full_tokens = count_tokens(&content);
    let outline_tokens = count_tokens(&outline);
    let savings = protocol::format_savings(full_tokens, outline_tokens);

    let filename = Path::new(path)
        .file_name()
        .and_then(|f| f.to_str())
        .unwrap_or(path);

    let line_count = content.lines().count();

    let header = format!(
        "{filename} ({line_count}L, {full_tokens} tok) — {} symbols{}",
        filtered.len(),
        kind_filter
            .map(|k| format!(" [filter: {k}]"))
            .unwrap_or_default()
    );

    (format!("{header}\n{outline}\n{savings}"), full_tokens)
}

fn filter_by_kind<'a>(sigs: &'a [Signature], kind: Option<&str>) -> Vec<&'a Signature> {
    match kind {
        None | Some("all") => sigs.iter().collect(),
        Some(k) => {
            let k_lower = k.to_lowercase();
            sigs.iter()
                .filter(|s| s.kind.to_lowercase() == k_lower)
                .collect()
        }
    }
}

fn format_outline(sigs: &[&Signature], _path: &str, crp: CrpMode) -> String {
    sigs.iter()
        .map(|s| {
            if crp.is_tdd() {
                s.to_tdd()
            } else {
                s.to_compact()
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::signatures::Signature;

    fn sample_sigs() -> Vec<Signature> {
        vec![
            Signature {
                kind: "fn",
                name: "main".to_string(),
                params: String::new(),
                return_type: String::new(),
                is_async: false,
                is_exported: false,
                indent: 0,
            },
            Signature {
                kind: "struct",
                name: "Config".to_string(),
                params: String::new(),
                return_type: String::new(),
                is_async: false,
                is_exported: true,
                indent: 0,
            },
            Signature {
                kind: "fn",
                name: "load".to_string(),
                params: "path: &str".to_string(),
                return_type: "Self".to_string(),
                is_async: false,
                is_exported: true,
                indent: 2,
            },
        ]
    }

    #[test]
    fn filter_fn_only() {
        let sigs = sample_sigs();
        let filtered = filter_by_kind(&sigs, Some("fn"));
        assert_eq!(filtered.len(), 2);
    }

    #[test]
    fn filter_struct_only() {
        let sigs = sample_sigs();
        let filtered = filter_by_kind(&sigs, Some("struct"));
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].name, "Config");
    }

    #[test]
    fn filter_all_returns_everything() {
        let sigs = sample_sigs();
        let filtered = filter_by_kind(&sigs, Some("all"));
        assert_eq!(filtered.len(), 3);
    }

    #[test]
    fn filter_none_returns_everything() {
        let sigs = sample_sigs();
        let filtered = filter_by_kind(&sigs, None);
        assert_eq!(filtered.len(), 3);
    }
}