lean_ctx/core/
predictive_coding.rs1use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
14pub struct ModeDelta {
15 pub mode: String,
16 pub added_lines: Vec<String>,
17 pub removed_lines: Vec<String>,
18 pub changed_lines: Vec<(String, String)>, pub unchanged_count: usize,
20}
21
22impl ModeDelta {
23 pub fn format_compact(&self) -> String {
25 let mut out = String::new();
26 out.push_str(&format!("[delta:{}] ", self.mode));
27 out.push_str(&format!("unchanged:{} ", self.unchanged_count));
28
29 if !self.added_lines.is_empty() {
30 out.push_str(&format!("+{} ", self.added_lines.len()));
31 }
32 if !self.removed_lines.is_empty() {
33 out.push_str(&format!("-{} ", self.removed_lines.len()));
34 }
35 if !self.changed_lines.is_empty() {
36 out.push_str(&format!("~{} ", self.changed_lines.len()));
37 }
38 out.push('\n');
39
40 for line in &self.added_lines {
41 out.push_str(&format!("+ {line}\n"));
42 }
43 for line in &self.removed_lines {
44 out.push_str(&format!("- {line}\n"));
45 }
46 for (old, new) in &self.changed_lines {
47 out.push_str(&format!("~ {old}\nā {new}\n"));
48 }
49
50 out
51 }
52
53 pub fn token_savings_estimate(&self, full_output_tokens: usize) -> f64 {
55 let delta_lines =
56 self.added_lines.len() + self.removed_lines.len() + self.changed_lines.len() * 2;
57 let delta_approx_tokens = delta_lines * 10; if full_output_tokens == 0 {
59 return 0.0;
60 }
61 1.0 - (delta_approx_tokens as f64 / full_output_tokens as f64).min(1.0)
62 }
63}
64
65pub fn compute_delta(mode: &str, previous: &str, current: &str) -> Option<ModeDelta> {
68 if previous == current {
69 return Some(ModeDelta {
70 mode: mode.to_string(),
71 added_lines: Vec::new(),
72 removed_lines: Vec::new(),
73 changed_lines: Vec::new(),
74 unchanged_count: current.lines().count(),
75 });
76 }
77
78 let prev_lines: Vec<&str> = previous.lines().collect();
79 let curr_lines: Vec<&str> = current.lines().collect();
80
81 let mut added = Vec::new();
82 let mut removed = Vec::new();
83 let mut changed = Vec::new();
84 let mut unchanged = 0;
85
86 let prev_set: HashMap<&str, usize> = prev_lines
88 .iter()
89 .enumerate()
90 .map(|(i, &l)| (l, i))
91 .collect();
92
93 for &line in &curr_lines {
94 if prev_set.contains_key(line) {
95 unchanged += 1;
96 } else {
97 added.push(line.to_string());
98 }
99 }
100
101 let curr_set: HashMap<&str, usize> = curr_lines
102 .iter()
103 .enumerate()
104 .map(|(i, &l)| (l, i))
105 .collect();
106
107 for &line in &prev_lines {
108 if !curr_set.contains_key(line) {
109 removed.push(line.to_string());
110 }
111 }
112
113 let min_len = prev_lines.len().min(curr_lines.len());
115 for i in 0..min_len {
116 if prev_lines[i] != curr_lines[i]
117 && !added.contains(&curr_lines[i].to_string())
118 && !removed.contains(&prev_lines[i].to_string())
119 {
120 changed.push((prev_lines[i].to_string(), curr_lines[i].to_string()));
121 }
122 }
123
124 Some(ModeDelta {
125 mode: mode.to_string(),
126 added_lines: added,
127 removed_lines: removed,
128 changed_lines: changed,
129 unchanged_count: unchanged,
130 })
131}
132
133pub fn should_use_delta(delta: &ModeDelta, full_output_tokens: usize) -> bool {
136 let savings = delta.token_savings_estimate(full_output_tokens);
137 savings > 0.30
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn identical_outputs_produce_zero_delta() {
147 let content = "fn main() {}\nfn helper() {}";
148 let delta = compute_delta("map", content, content).unwrap();
149 assert!(delta.added_lines.is_empty());
150 assert!(delta.removed_lines.is_empty());
151 assert!(delta.changed_lines.is_empty());
152 assert_eq!(delta.unchanged_count, 2);
153 }
154
155 #[test]
156 fn added_lines_detected() {
157 let prev = "fn main() {}";
158 let curr = "fn main() {}\nfn new_fn() {}";
159 let delta = compute_delta("signatures", prev, curr).unwrap();
160 assert_eq!(delta.added_lines.len(), 1);
161 assert!(delta.added_lines[0].contains("new_fn"));
162 }
163
164 #[test]
165 fn removed_lines_detected() {
166 let prev = "fn main() {}\nfn old_fn() {}";
167 let curr = "fn main() {}";
168 let delta = compute_delta("map", prev, curr).unwrap();
169 assert_eq!(delta.removed_lines.len(), 1);
170 assert!(delta.removed_lines[0].contains("old_fn"));
171 }
172
173 #[test]
174 fn savings_estimate_makes_sense() {
175 let delta = ModeDelta {
176 mode: "map".into(),
177 added_lines: vec!["new".into()],
178 removed_lines: Vec::new(),
179 changed_lines: Vec::new(),
180 unchanged_count: 50,
181 };
182 let savings = delta.token_savings_estimate(500);
183 assert!(savings > 0.9); }
185
186 #[test]
187 fn compact_format_is_readable() {
188 let delta = ModeDelta {
189 mode: "signatures".into(),
190 added_lines: vec!["+ pub fn new_api()".into()],
191 removed_lines: vec!["- pub fn old_api()".into()],
192 changed_lines: Vec::new(),
193 unchanged_count: 10,
194 };
195 let formatted = delta.format_compact();
196 assert!(formatted.contains("[delta:signatures]"));
197 assert!(formatted.contains("unchanged:10"));
198 }
199}