lean-ctx 3.6.5

Context Runtime for AI Agents with CCP. 51 MCP tools, 10 read modes, 60+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, 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 super::model::{Gotcha, GotchaStore};

/// A distilled learning from error-resolution correlation.
pub struct Learning {
    pub category: String,
    pub trigger: String,
    pub resolution: String,
    pub confidence: f32,
    pub occurrences: u32,
    pub sessions: usize,
}

impl std::fmt::Display for Learning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "[{cat}] {trigger}{res} (confidence: {conf:.0}%, seen {occ}x across {sess} sessions)",
            cat = self.category,
            trigger = self.trigger,
            res = self.resolution,
            conf = self.confidence * 100.0,
            occ = self.occurrences,
            sess = self.sessions,
        )
    }
}

const MIN_CONFIDENCE: f32 = 0.5;
const MIN_OCCURRENCES: u32 = 2;

/// Extract high-confidence learnings from the gotcha store.
pub fn extract_learnings(store: &GotchaStore) -> Vec<Learning> {
    store
        .gotchas
        .iter()
        .filter(|g| g.confidence >= MIN_CONFIDENCE && g.occurrences >= MIN_OCCURRENCES)
        .map(gotcha_to_learning)
        .collect()
}

fn gotcha_to_learning(g: &Gotcha) -> Learning {
    Learning {
        category: g.category.short_label().to_string(),
        trigger: g.trigger.clone(),
        resolution: g.resolution.clone(),
        confidence: g.confidence,
        occurrences: g.occurrences,
        sessions: g.session_ids.len(),
    }
}

const AGENTS_MARKER_START: &str = "<!-- lean-ctx-learn-start -->";
const AGENTS_MARKER_END: &str = "<!-- lean-ctx-learn-end -->";

/// Generate the markdown section to inject into AGENTS.md.
pub fn format_agents_section(learnings: &[Learning]) -> String {
    if learnings.is_empty() {
        return String::new();
    }

    let mut out = String::new();
    out.push_str(AGENTS_MARKER_START);
    out.push('\n');
    out.push_str("## Learned Gotchas (auto-generated by `lean-ctx learn`)\n\n");
    out.push_str("Do NOT edit this section manually — it is overwritten on each `lean-ctx learn --apply`.\n\n");

    for l in learnings {
        out.push_str(&format!(
            "- **[{cat}]** {trigger}\n{res}\n",
            cat = l.category,
            trigger = l.trigger,
            res = l.resolution,
        ));
    }
    out.push_str(AGENTS_MARKER_END);
    out.push('\n');
    out
}

/// Write learnings into an AGENTS.md file, replacing any existing marker section.
pub fn apply_to_agents_md(project_root: &str, learnings: &[Learning]) -> Result<String, String> {
    let agents_path = std::path::Path::new(project_root).join("AGENTS.md");

    let existing = if agents_path.exists() {
        std::fs::read_to_string(&agents_path)
            .map_err(|e| format!("Failed to read AGENTS.md: {e}"))?
    } else {
        String::new()
    };

    let section = format_agents_section(learnings);
    if section.is_empty() {
        return Ok("No learnings to write (need >=2 occurrences with >=50% confidence).".into());
    }

    let updated = if existing.contains(AGENTS_MARKER_START) {
        let before = existing
            .split(AGENTS_MARKER_START)
            .next()
            .unwrap_or(&existing);
        let after = existing.split(AGENTS_MARKER_END).nth(1).unwrap_or("");
        format!(
            "{}{}{}",
            before.trim_end(),
            "\n\n",
            section.trim_end().to_owned() + after
        )
    } else if existing.is_empty() {
        format!("# AGENTS.md\n\n{section}")
    } else {
        format!("{}\n\n{section}", existing.trim_end())
    };

    std::fs::write(&agents_path, &updated)
        .map_err(|e| format!("Failed to write AGENTS.md: {e}"))?;

    Ok(format!(
        "Wrote {} learnings to {}",
        learnings.len(),
        agents_path.display()
    ))
}