Skip to main content

lean_ctx/core/gotcha_tracker/
learn.rs

1use super::model::{Gotcha, GotchaStore};
2
3/// A distilled learning from error-resolution correlation.
4pub struct Learning {
5    pub category: String,
6    pub trigger: String,
7    pub resolution: String,
8    pub confidence: f32,
9    pub occurrences: u32,
10    pub sessions: usize,
11}
12
13impl std::fmt::Display for Learning {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(
16            f,
17            "[{cat}] {trigger} → {res} (confidence: {conf:.0}%, seen {occ}x across {sess} sessions)",
18            cat = self.category,
19            trigger = self.trigger,
20            res = self.resolution,
21            conf = self.confidence * 100.0,
22            occ = self.occurrences,
23            sess = self.sessions,
24        )
25    }
26}
27
28const MIN_CONFIDENCE: f32 = 0.5;
29const MIN_OCCURRENCES: u32 = 2;
30
31/// Extract high-confidence learnings from the gotcha store.
32pub fn extract_learnings(store: &GotchaStore) -> Vec<Learning> {
33    store
34        .gotchas
35        .iter()
36        .filter(|g| g.confidence >= MIN_CONFIDENCE && g.occurrences >= MIN_OCCURRENCES)
37        .map(gotcha_to_learning)
38        .collect()
39}
40
41fn gotcha_to_learning(g: &Gotcha) -> Learning {
42    Learning {
43        category: g.category.short_label().to_string(),
44        trigger: g.trigger.clone(),
45        resolution: g.resolution.clone(),
46        confidence: g.confidence,
47        occurrences: g.occurrences,
48        sessions: g.session_ids.len(),
49    }
50}
51
52const AGENTS_MARKER_START: &str = "<!-- lean-ctx-learn-start -->";
53const AGENTS_MARKER_END: &str = "<!-- lean-ctx-learn-end -->";
54
55/// Generate the markdown section to inject into AGENTS.md.
56pub fn format_agents_section(learnings: &[Learning]) -> String {
57    if learnings.is_empty() {
58        return String::new();
59    }
60
61    let mut out = String::new();
62    out.push_str(AGENTS_MARKER_START);
63    out.push('\n');
64    out.push_str("## Learned Gotchas (auto-generated by `lean-ctx learn`)\n\n");
65    out.push_str("Do NOT edit this section manually — it is overwritten on each `lean-ctx learn --apply`.\n\n");
66
67    for l in learnings {
68        out.push_str(&format!(
69            "- **[{cat}]** {trigger}\n  → {res}\n",
70            cat = l.category,
71            trigger = l.trigger,
72            res = l.resolution,
73        ));
74    }
75    out.push_str(AGENTS_MARKER_END);
76    out.push('\n');
77    out
78}
79
80/// Write learnings into an AGENTS.md file, replacing any existing marker section.
81pub fn apply_to_agents_md(project_root: &str, learnings: &[Learning]) -> Result<String, String> {
82    let agents_path = std::path::Path::new(project_root).join("AGENTS.md");
83
84    let existing = if agents_path.exists() {
85        std::fs::read_to_string(&agents_path)
86            .map_err(|e| format!("Failed to read AGENTS.md: {e}"))?
87    } else {
88        String::new()
89    };
90
91    let section = format_agents_section(learnings);
92    if section.is_empty() {
93        return Ok("No learnings to write (need >=2 occurrences with >=50% confidence).".into());
94    }
95
96    let updated = if existing.contains(AGENTS_MARKER_START) {
97        let before = existing
98            .split(AGENTS_MARKER_START)
99            .next()
100            .unwrap_or(&existing);
101        let after = existing.split(AGENTS_MARKER_END).nth(1).unwrap_or("");
102        format!(
103            "{}{}{}",
104            before.trim_end(),
105            "\n\n",
106            section.trim_end().to_owned() + after
107        )
108    } else if existing.is_empty() {
109        format!("# AGENTS.md\n\n{section}")
110    } else {
111        format!("{}\n\n{section}", existing.trim_end())
112    };
113
114    std::fs::write(&agents_path, &updated)
115        .map_err(|e| format!("Failed to write AGENTS.md: {e}"))?;
116
117    Ok(format!(
118        "Wrote {} learnings to {}",
119        learnings.len(),
120        agents_path.display()
121    ))
122}