leankg 0.12.2

Lightweight Knowledge Graph for AI-Assisted Development
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReadMode {
    Adaptive,
    Full,
    Map,
    Signatures,
    Diff,
    Aggressive,
    Entropy,
    Lines,
}

impl Default for ReadMode {
    fn default() -> Self {
        ReadMode::Adaptive
    }
}

impl ReadMode {
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "adaptive" => Some(ReadMode::Adaptive),
            "full" => Some(ReadMode::Full),
            "map" => Some(ReadMode::Map),
            "signatures" => Some(ReadMode::Signatures),
            "diff" => Some(ReadMode::Diff),
            "aggressive" => Some(ReadMode::Aggressive),
            "entropy" => Some(ReadMode::Entropy),
            "lines" => Some(ReadMode::Lines),
            _ => None,
        }
    }

    pub fn description(&self) -> &'static str {
        match self {
            ReadMode::Adaptive => "Auto-select best mode based on file type/size/cache",
            ReadMode::Full => "Complete file content (cached re-reads ≈ 13 tokens)",
            ReadMode::Map => "Dependencies + exports + API signatures (~85-95% savings)",
            ReadMode::Signatures => "Function/class signatures only (~90-95% savings)",
            ReadMode::Diff => "Only changed hunks via Myers diff",
            ReadMode::Aggressive => "Syntax-stripped, removes boilerplate (~60-70% savings)",
            ReadMode::Entropy => {
                "Shannon entropy filtered for repetitive patterns (~70-80% savings)"
            }
            ReadMode::Lines => "Specific line ranges (proportional savings)",
        }
    }

    pub fn estimated_savings(&self) -> &'static str {
        match self {
            ReadMode::Adaptive => "~75-95% (auto-selected)",
            ReadMode::Full => "~0% cached",
            ReadMode::Map => "~85-95%",
            ReadMode::Signatures => "~90-95%",
            ReadMode::Diff => "proportional",
            ReadMode::Aggressive => "~60-70%",
            ReadMode::Entropy => "~70-80%",
            ReadMode::Lines => "proportional",
        }
    }

    pub fn select_adaptive(file_path: &str, _file_size: usize, lines: usize) -> Self {
        let extension = std::path::Path::new(file_path)
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");

        let code_extensions = ["rs", "go", "ts", "js", "py", "java", "c", "cpp", "h"];
        let is_code = code_extensions.contains(&extension);

        if !is_code {
            return ReadMode::Full;
        }

        if lines > 500 {
            ReadMode::Map
        } else if lines > 200 {
            ReadMode::Signatures
        } else {
            ReadMode::Map
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinesRange {
    pub start: usize,
    pub end: usize,
}

impl LinesRange {
    pub fn parse(s: &str) -> Option<Self> {
        let parts: Vec<&str> = s.split('-').collect();
        if parts.len() != 2 {
            return None;
        }
        let start = parts[0].parse().ok()?;
        let end = parts[1].parse().ok()?;
        if start > end {
            return None;
        }
        Some(LinesRange { start, end })
    }
}

pub fn parse_lines_spec(spec: &str) -> Vec<LinesRange> {
    let mut ranges = Vec::new();
    for part in spec.split(',') {
        if let Some(range) = LinesRange::parse(part.trim()) {
            ranges.push(range);
        }
    }
    ranges
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_read_mode_from_str() {
        assert_eq!(ReadMode::from_str("full"), Some(ReadMode::Full));
        assert_eq!(ReadMode::from_str("map"), Some(ReadMode::Map));
        assert_eq!(ReadMode::from_str("SIGNATURES"), Some(ReadMode::Signatures));
        assert_eq!(ReadMode::from_str("invalid"), None);
    }

    #[test]
    fn test_lines_range_parse() {
        assert_eq!(
            LinesRange::parse("10-20"),
            Some(LinesRange { start: 10, end: 20 })
        );
        assert_eq!(
            LinesRange::parse("5-5"),
            Some(LinesRange { start: 5, end: 5 })
        );
        assert_eq!(LinesRange::parse("20-10"), None);
        assert_eq!(LinesRange::parse("invalid"), None);
    }

    #[test]
    fn test_parse_lines_spec() {
        let ranges = parse_lines_spec("10-20,30-40,50-60");
        assert_eq!(ranges.len(), 3);
        assert_eq!(ranges[0].start, 10);
        assert_eq!(ranges[2].end, 60);
    }
}