lean_ctx/core/
rules_channel.rs1use std::path::Path;
17
18pub use crate::core::rules_canonical::{COMPRESSION_BLOCK_END, COMPRESSION_BLOCK_START};
23
24pub const AGENTS_MD_READERS: &[&str] = &["cursor", "codex"];
28
29pub fn carries_full_rules(content: &str) -> bool {
37 content.contains(crate::core::rules_canonical::START_MARK)
38 || content.contains(COMPRESSION_BLOCK_START)
39}
40
41pub fn is_pointer_only(content: &str) -> bool {
44 content.contains("<!-- lean-ctx") && !carries_full_rules(content)
45}
46
47fn file_has_compression(path: &Path) -> bool {
48 std::fs::read_to_string(path).is_ok_and(|c| c.contains(COMPRESSION_BLOCK_START))
49}
50
51pub fn cursor_compression_covered(home: &Path) -> bool {
54 file_has_compression(&home.join(".cursor/rules/lean-ctx.mdc"))
55}
56
57fn codex_dir(home: &Path) -> std::path::PathBuf {
59 crate::core::home::resolve_codex_dir().unwrap_or_else(|| home.join(".codex"))
60}
61
62pub fn codex_present(home: &Path) -> bool {
64 codex_dir(home).exists()
65}
66
67pub fn codex_compression_covered(home: &Path) -> bool {
69 file_has_compression(&codex_dir(home).join("AGENTS.md"))
70}
71
72pub fn agents_md_can_thin(home: &Path) -> bool {
80 if !cursor_compression_covered(home) {
81 return false;
82 }
83 if codex_present(home) && !codex_compression_covered(home) {
84 return false;
85 }
86 true
87}
88
89pub fn client_autoloads_compression(client_name: &str, home: &Path) -> bool {
94 let lower = client_name.to_lowercase();
95 if lower.is_empty() {
96 return false;
97 }
98 if lower.contains("cursor") {
99 return cursor_compression_covered(home);
100 }
101 if lower.contains("codex") {
102 return codex_present(home) && codex_compression_covered(home);
103 }
104 false
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 const FULL_HEADER: &str = crate::core::rules_canonical::START_MARK;
112
113 fn compression_block() -> String {
114 format!("{COMPRESSION_BLOCK_START}\nOUTPUT STYLE\n{COMPRESSION_BLOCK_END}\n")
115 }
116
117 fn pointer_block() -> String {
118 format!(
119 "{}\n## lean-ctx\nFull rules: ~/.cursor/rules/lean-ctx.mdc\n{}\n",
120 crate::core::rules_canonical::AGENTS_BLOCK_START,
121 crate::core::rules_canonical::AGENTS_BLOCK_END,
122 )
123 }
124
125 #[test]
126 fn full_rules_detected_for_canonical_header_and_compression() {
127 let comp = compression_block();
128 let ptr = pointer_block();
129 assert!(carries_full_rules(&format!("{FULL_HEADER}\nbody\n")));
130 assert!(carries_full_rules(&comp));
131 assert!(carries_full_rules(&format!("{ptr}{comp}")));
132 }
133
134 #[test]
135 fn pointer_only_block_is_not_full() {
136 let ptr = pointer_block();
137 assert!(!carries_full_rules(&ptr));
138 assert!(is_pointer_only(&ptr));
139 }
140
141 #[test]
142 fn plain_user_content_is_neither_full_nor_pointer() {
143 let user = "# My project rules\njust some notes\n";
144 assert!(!carries_full_rules(user));
145 assert!(!is_pointer_only(user));
146 }
147
148 #[test]
149 fn cursor_coverage_follows_mdc_block() {
150 let comp = compression_block();
151 let tmp = tempfile::tempdir().unwrap();
152 let home = tmp.path();
153 assert!(!cursor_compression_covered(home));
154
155 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
156 std::fs::write(
157 home.join(".cursor/rules/lean-ctx.mdc"),
158 format!("{FULL_HEADER}\n{comp}"),
159 )
160 .unwrap();
161 assert!(cursor_compression_covered(home));
162 }
163
164 #[test]
165 fn agents_md_thins_only_when_cursor_covered_and_no_uncovered_codex() {
166 let comp = compression_block();
167 let _guard = crate::core::data_dir::test_env_lock();
169 let tmp = tempfile::tempdir().unwrap();
170 let home = tmp.path();
171
172 crate::test_env::set_var("CODEX_HOME", home.join(".codex"));
174 assert!(!agents_md_can_thin(home));
175
176 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
180 std::fs::write(
181 home.join(".cursor/rules/lean-ctx.mdc"),
182 format!("{FULL_HEADER}\n{comp}"),
183 )
184 .unwrap();
185 assert!(agents_md_can_thin(home));
186
187 std::fs::create_dir_all(home.join(".codex")).unwrap();
189 assert!(codex_present(home));
190 assert!(!agents_md_can_thin(home));
191
192 std::fs::write(home.join(".codex/AGENTS.md"), &comp).unwrap();
194 assert!(agents_md_can_thin(home));
195 crate::test_env::remove_var("CODEX_HOME");
196 }
197
198 #[test]
199 fn client_autoloads_compression_is_client_aware() {
200 let comp = compression_block();
201 let tmp = tempfile::tempdir().unwrap();
202 let home = tmp.path();
203 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
204 std::fs::write(
205 home.join(".cursor/rules/lean-ctx.mdc"),
206 format!("{FULL_HEADER}\n{comp}"),
207 )
208 .unwrap();
209
210 assert!(client_autoloads_compression("Cursor", home));
211 assert!(client_autoloads_compression("cursor-vscode", home));
212 assert!(!client_autoloads_compression("", home));
214 assert!(!client_autoloads_compression("some-other-agent", home));
215 }
216
217 #[test]
218 fn render_output_is_detected_as_compression_coverage() {
219 use crate::core::config::CompressionLevel;
226 use crate::core::rules_canonical::{Wrapper, render};
227
228 let tmp = tempfile::tempdir().unwrap();
229 let home = tmp.path();
230 assert!(!cursor_compression_covered(home));
231
232 let block = render(false, Wrapper::Dedicated, CompressionLevel::Standard);
235 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
236 std::fs::write(home.join(".cursor/rules/lean-ctx.mdc"), &block).unwrap();
237
238 assert!(cursor_compression_covered(home));
239 assert!(client_autoloads_compression("cursor", home));
240
241 let off = render(false, Wrapper::Dedicated, CompressionLevel::Off);
243 std::fs::write(home.join(".cursor/rules/lean-ctx.mdc"), &off).unwrap();
244 assert!(!cursor_compression_covered(home));
245 }
246}