lean_ctx_proxy/
prose_patterns.rs1use std::collections::HashSet;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct PatternCompression {
13 pub compressed: String,
14 pub units_removed: usize,
15}
16
17pub fn compress_prose_patterns(content: &str) -> PatternCompression {
19 if content.trim().is_empty() {
20 return PatternCompression {
21 compressed: content.to_owned(),
22 units_removed: 0,
23 };
24 }
25
26 let mut compressed = String::with_capacity(content.len());
27 let mut prose = Vec::new();
28 let mut in_fence = false;
29 let mut units_removed = 0usize;
30
31 for line in content.split_inclusive('\n') {
32 if is_fence(line) {
33 let result = compress_prose_segment(&prose.join(""));
34 compressed.push_str(&result.compressed);
35 units_removed += result.units_removed;
36 prose.clear();
37 compressed.push_str(line);
38 in_fence = !in_fence;
39 } else if in_fence {
40 compressed.push_str(line);
41 } else {
42 prose.push(line);
43 }
44 }
45
46 let result = compress_prose_segment(&prose.join(""));
47 compressed.push_str(&result.compressed);
48 units_removed += result.units_removed;
49
50 PatternCompression {
51 compressed,
52 units_removed,
53 }
54}
55
56pub fn remove_seen_instruction_lines(content: &mut String, seen: &mut HashSet<String>) -> usize {
62 let mut retained = String::with_capacity(content.len());
63 let mut units_removed = 0;
64
65 for line in content.split_inclusive('\n') {
66 if is_instruction_line(line) {
67 let key = canonical(line);
68 if !key.is_empty() && !seen.insert(key) {
69 units_removed += 1;
70 continue;
71 }
72 }
73 retained.push_str(line);
74 }
75
76 if units_removed > 0 {
77 *content = retained;
78 }
79 units_removed
80}
81
82fn compress_prose_segment(segment: &str) -> PatternCompression {
83 if segment.trim().is_empty() {
84 return PatternCompression {
85 compressed: segment.to_owned(),
86 units_removed: 0,
87 };
88 }
89
90 let mut seen_headings = HashSet::new();
91 let mut seen_paragraphs = HashSet::new();
92 let mut seen_instruction_lines = HashSet::new();
93 let mut retained = Vec::new();
94 let mut paragraph = Vec::new();
95 let mut units_removed = 0usize;
96
97 let flush_paragraph = |paragraph: &mut Vec<&str>,
98 retained: &mut Vec<String>,
99 seen_paragraphs: &mut HashSet<String>,
100 units_removed: &mut usize| {
101 if paragraph.is_empty() {
102 return;
103 }
104 let original = paragraph.join("");
105 paragraph.clear();
106 let result = compress_paragraph(&original);
107 let key = canonical(&result.compressed);
108 if !key.is_empty() && !seen_paragraphs.insert(key) {
109 *units_removed += 1;
110 return;
111 }
112 *units_removed += result.units_removed;
113 retained.push(result.compressed);
114 };
115
116 for line in segment.split_inclusive('\n') {
117 if line.trim().is_empty() {
118 flush_paragraph(
119 &mut paragraph,
120 &mut retained,
121 &mut seen_paragraphs,
122 &mut units_removed,
123 );
124 retained.push(line.to_owned());
125 continue;
126 }
127
128 if is_heading(line) {
129 flush_paragraph(
130 &mut paragraph,
131 &mut retained,
132 &mut seen_paragraphs,
133 &mut units_removed,
134 );
135 let key = canonical(line.trim_start_matches('#').trim());
136 if key.is_empty() || seen_headings.insert(key) {
137 retained.push(line.to_owned());
138 } else {
139 units_removed += 1;
140 }
141 continue;
142 }
143
144 if is_instruction_line(line) {
145 flush_paragraph(
146 &mut paragraph,
147 &mut retained,
148 &mut seen_paragraphs,
149 &mut units_removed,
150 );
151 let key = canonical(line);
152 if key.is_empty() || seen_instruction_lines.insert(key) {
153 retained.push(line.to_owned());
154 } else {
155 units_removed += 1;
156 }
157 continue;
158 }
159
160 paragraph.push(line);
161 }
162
163 flush_paragraph(
164 &mut paragraph,
165 &mut retained,
166 &mut seen_paragraphs,
167 &mut units_removed,
168 );
169
170 PatternCompression {
171 compressed: retained.concat(),
172 units_removed,
173 }
174}
175
176fn compress_paragraph(paragraph: &str) -> PatternCompression {
177 let sentences = sentences(paragraph);
178 if sentences.len() < 2 {
179 return PatternCompression {
180 compressed: paragraph.to_owned(),
181 units_removed: 0,
182 };
183 }
184
185 let error = paragraph_has_error(paragraph);
186 let mut retained = Vec::new();
187 let mut previous = Vec::<HashSet<String>>::new();
188 let mut units_removed = 0usize;
189
190 for (index, sentence) in sentences.iter().enumerate() {
191 let words = word_set(sentence);
192 let duplicate = words.len() >= 6
193 && previous
194 .iter()
195 .any(|candidate| overlap_ratio(&words, candidate) >= 0.82);
196 let verbose_error_detail = error
197 && index > 0
198 && index + 1 < sentences.len()
199 && words.len() >= 12
200 && !contains_remediation(sentence);
201
202 if duplicate || verbose_error_detail {
203 units_removed += 1;
204 continue;
205 }
206
207 previous.push(words);
208 retained.push(*sentence);
209 }
210
211 if retained.is_empty() {
212 return PatternCompression {
213 compressed: sentences[0].to_owned(),
214 units_removed: units_removed.saturating_sub(1),
215 };
216 }
217
218 PatternCompression {
219 compressed: retained.join(" "),
220 units_removed,
221 }
222}
223
224fn sentences(paragraph: &str) -> Vec<&str> {
225 let mut result = Vec::new();
226 let mut start = 0;
227 for (index, character) in paragraph.char_indices() {
228 if matches!(character, '.' | '!' | '?') {
229 let end = index + character.len_utf8();
230 if let Some(sentence) = paragraph.get(start..end) {
231 if !sentence.trim().is_empty() {
232 result.push(sentence.trim());
233 }
234 }
235 start = end;
236 }
237 }
238 if let Some(tail) = paragraph.get(start..) {
239 if !tail.trim().is_empty() {
240 result.push(tail.trim());
241 }
242 }
243 result
244}
245
246fn canonical(text: &str) -> String {
247 text.chars()
248 .flat_map(char::to_lowercase)
249 .map(|character| {
250 if character.is_alphanumeric() {
251 character
252 } else {
253 ' '
254 }
255 })
256 .collect::<String>()
257 .split_whitespace()
258 .collect::<Vec<_>>()
259 .join(" ")
260}
261
262fn word_set(text: &str) -> HashSet<String> {
263 canonical(text)
264 .split_whitespace()
265 .filter(|word| word.len() > 2)
266 .map(ToOwned::to_owned)
267 .collect()
268}
269
270fn overlap_ratio(left: &HashSet<String>, right: &HashSet<String>) -> f32 {
271 let union = left.union(right).count();
272 if union == 0 {
273 return 0.0;
274 }
275 left.intersection(right).count() as f32 / union as f32
276}
277
278fn is_fence(line: &str) -> bool {
279 let trimmed = line.trim_start();
280 trimmed.starts_with("```") || trimmed.starts_with("~~~")
281}
282
283fn is_heading(line: &str) -> bool {
284 let trimmed = line.trim_start();
285 trimmed.starts_with('#') && trimmed.trim_start_matches('#').starts_with(' ')
286}
287
288fn is_instruction_line(line: &str) -> bool {
289 let words = canonical(line);
290 let instruction_prefixes = [
291 "please ",
292 "always ",
293 "never ",
294 "must ",
295 "do not ",
296 "remember to ",
297 "make sure ",
298 "follow ",
299 ];
300 instruction_prefixes
301 .iter()
302 .any(|prefix| words.starts_with(prefix))
303}
304
305fn paragraph_has_error(paragraph: &str) -> bool {
306 sentence_has_error(paragraph)
307}
308
309fn sentence_has_error(sentence: &str) -> bool {
310 let lower = canonical(sentence);
311 [
312 "error",
313 "exception",
314 "failed",
315 "failure",
316 "panic",
317 "traceback",
318 ]
319 .iter()
320 .any(|marker| lower.split_whitespace().any(|word| word == *marker))
321}
322
323fn contains_remediation(sentence: &str) -> bool {
324 let lower = canonical(sentence);
325 [
326 "fix ", "retry ", "resolve ", "run ", "use ", "change ", "set ", "remove ",
327 ]
328 .iter()
329 .any(|prefix| lower.starts_with(prefix))
330}
331
332#[cfg(test)]
333mod tests {
334 use super::compress_prose_patterns;
335
336 #[test]
337 fn removes_repeated_explanations_but_keeps_the_first_one() {
338 let source = "The cache stores normalized keys before lookup. The cache stores normalized keys before lookup. Keep the key format stable.";
339 let result = compress_prose_patterns(source);
340 assert_eq!(
341 result.compressed,
342 "The cache stores normalized keys before lookup. Keep the key format stable."
343 );
344 assert_eq!(result.units_removed, 1);
345 }
346
347 #[test]
348 fn collapses_repeated_markdown_headings_and_instruction_lines() {
349 let source = "# Rules\nAlways run tests before merging.\n# Rules\nAlways run tests before merging.\n";
350 let result = compress_prose_patterns(source);
351 assert_eq!(
352 result.compressed,
353 "# Rules\nAlways run tests before merging.\n"
354 );
355 assert_eq!(result.units_removed, 2);
356 }
357
358 #[test]
359 fn keeps_the_error_and_remediation_while_dropping_verbose_detail() {
360 let source = "Error: cache write failed. The worker attempted every available fallback and recorded extensive retry diagnostics for each failed filesystem operation. Retry after freeing disk space.";
361 let result = compress_prose_patterns(source);
362 assert_eq!(
363 result.compressed,
364 "Error: cache write failed. Retry after freeing disk space."
365 );
366 assert_eq!(result.units_removed, 1);
367 }
368
369 #[test]
370 fn code_fences_are_byte_preserved() {
371 let source = "Repeat this explanation. Repeat this explanation.\n```rust\nlet value = \"Repeat this explanation.\";\n```\n";
372 let result = compress_prose_patterns(source);
373 assert!(
374 result
375 .compressed
376 .contains("let value = \"Repeat this explanation.\";")
377 );
378 }
379
380 #[test]
381 fn removes_boilerplate_instruction_lines_seen_in_earlier_turns() {
382 let mut seen = std::collections::HashSet::new();
383 let mut first = "Always run tests before merging.\nKeep the failure output.\n".to_owned();
384 let mut second = "Always run tests before merging.\nKeep the failure output.\n".to_owned();
385 assert_eq!(
386 super::remove_seen_instruction_lines(&mut first, &mut seen),
387 0
388 );
389 assert_eq!(
390 super::remove_seen_instruction_lines(&mut second, &mut seen),
391 1
392 );
393 assert_eq!(second, "Keep the failure output.\n");
394 }
395}