1use std::path::Path;
2
3pub fn detect_project_root(file_path: &str) -> Option<String> {
8 let mut dir = Path::new(file_path).parent()?;
9 let mut best: Option<String> = None;
10
11 loop {
12 if is_project_root_marker(dir) {
13 best = Some(dir.to_string_lossy().to_string());
14 }
15 match dir.parent() {
16 Some(parent) if parent != dir => dir = parent,
17 _ => break,
18 }
19 }
20 best
21}
22
23fn is_project_root_marker(dir: &Path) -> bool {
25 const MARKERS: &[&str] = &[
26 ".git",
27 "Cargo.toml",
28 "package.json",
29 "go.work",
30 "pnpm-workspace.yaml",
31 "lerna.json",
32 "nx.json",
33 "turbo.json",
34 ".projectile",
35 "pyproject.toml",
36 "setup.py",
37 "Makefile",
38 "CMakeLists.txt",
39 "BUILD.bazel",
40 ];
41 MARKERS.iter().any(|m| dir.join(m).exists())
42}
43
44pub fn detect_project_root_or_cwd(file_path: &str) -> String {
46 detect_project_root(file_path).unwrap_or_else(|| {
47 let p = Path::new(file_path);
48 if p.exists() {
49 if p.is_dir() {
50 return file_path.to_string();
51 }
52 if let Some(parent) = p.parent() {
53 return parent.to_string_lossy().to_string();
54 }
55 return file_path.to_string();
56 }
57 std::env::current_dir()
58 .map_or_else(|_| ".".to_string(), |p| p.to_string_lossy().to_string())
59 })
60}
61
62pub fn shorten_path(path: &str) -> String {
64 let p = Path::new(path);
65 if let Some(name) = p.file_name() {
66 return name.to_string_lossy().to_string();
67 }
68 path.to_string()
69}
70
71pub fn format_savings(original: usize, compressed: usize) -> String {
73 let saved = original.saturating_sub(compressed);
74 if original == 0 {
75 return "0 tok saved".to_string();
76 }
77 let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
78 format!("[{saved} tok saved ({pct}%)]")
79}
80
81pub struct InstructionTemplate {
83 pub code: &'static str,
84 pub full: &'static str,
85}
86
87const TEMPLATES: &[InstructionTemplate] = &[
88 InstructionTemplate {
89 code: "ACT1",
90 full: "Act immediately, 1-line result",
91 },
92 InstructionTemplate {
93 code: "BRIEF",
94 full: "1-2 line approach, then act",
95 },
96 InstructionTemplate {
97 code: "FULL",
98 full: "Outline+edge cases, then act",
99 },
100 InstructionTemplate {
101 code: "DELTA",
102 full: "Changed lines only",
103 },
104 InstructionTemplate {
105 code: "NOREPEAT",
106 full: "No repeat, use Fn refs",
107 },
108 InstructionTemplate {
109 code: "STRUCT",
110 full: "+/-/~ notation",
111 },
112 InstructionTemplate {
113 code: "1LINE",
114 full: "1 line per action",
115 },
116 InstructionTemplate {
117 code: "NODOC",
118 full: "No narration comments",
119 },
120 InstructionTemplate {
121 code: "ACTFIRST",
122 full: "Tool calls first, no narration",
123 },
124 InstructionTemplate {
125 code: "QUALITY",
126 full: "Never skip edge cases",
127 },
128 InstructionTemplate {
129 code: "NOMOCK",
130 full: "No mock/placeholder data",
131 },
132 InstructionTemplate {
133 code: "FREF",
134 full: "Fn refs only, no full paths",
135 },
136 InstructionTemplate {
137 code: "DIFF",
138 full: "Diff lines only",
139 },
140 InstructionTemplate {
141 code: "ABBREV",
142 full: "fn,cfg,impl,deps,req,res,ctx,err",
143 },
144 InstructionTemplate {
145 code: "SYMBOLS",
146 full: "+=add -=rm ~=mod ->=ret",
147 },
148];
149
150pub fn instruction_decoder_block() -> String {
152 let pairs: Vec<String> = TEMPLATES
153 .iter()
154 .map(|t| format!("{}={}", t.code, t.full))
155 .collect();
156 format!("INSTRUCTION CODES:\n {}", pairs.join(" | "))
157}
158
159pub fn encode_instructions(complexity: &str) -> String {
162 match complexity {
163 "mechanical" => "MODE: ACT1 DELTA 1LINE | BUDGET: <=50 tokens, 1 line answer".to_string(),
164 "simple" => "MODE: BRIEF DELTA 1LINE | BUDGET: <=100 tokens, structured".to_string(),
165 "standard" => "MODE: BRIEF DELTA NOREPEAT STRUCT | BUDGET: <=200 tokens".to_string(),
166 "complex" => {
167 "MODE: FULL QUALITY NOREPEAT STRUCT FREF DIFF | BUDGET: <=500 tokens".to_string()
168 }
169 "architectural" => {
170 "MODE: FULL QUALITY NOREPEAT STRUCT FREF | BUDGET: unlimited".to_string()
171 }
172 _ => "MODE: BRIEF | BUDGET: <=200 tokens".to_string(),
173 }
174}
175
176pub fn encode_instructions_with_snr(complexity: &str, compression_pct: f64) -> String {
178 let snr = if compression_pct > 0.0 {
179 1.0 - (compression_pct / 100.0)
180 } else {
181 1.0
182 };
183 let base = encode_instructions(complexity);
184 format!("{base} | SNR: {snr:.2}")
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn is_project_root_marker_detects_git() {
193 let tmp = std::env::temp_dir().join("lean-ctx-test-root-marker");
194 let _ = std::fs::create_dir_all(&tmp);
195 let git_dir = tmp.join(".git");
196 let _ = std::fs::create_dir_all(&git_dir);
197 assert!(is_project_root_marker(&tmp));
198 let _ = std::fs::remove_dir_all(&tmp);
199 }
200
201 #[test]
202 fn is_project_root_marker_detects_cargo_toml() {
203 let tmp = std::env::temp_dir().join("lean-ctx-test-cargo-marker");
204 let _ = std::fs::create_dir_all(&tmp);
205 let _ = std::fs::write(tmp.join("Cargo.toml"), "[package]");
206 assert!(is_project_root_marker(&tmp));
207 let _ = std::fs::remove_dir_all(&tmp);
208 }
209
210 #[test]
211 fn detect_project_root_finds_outermost() {
212 let base = std::env::temp_dir().join("lean-ctx-test-monorepo");
213 let inner = base.join("packages").join("app");
214 let _ = std::fs::create_dir_all(&inner);
215 let _ = std::fs::create_dir_all(base.join(".git"));
216 let _ = std::fs::create_dir_all(inner.join(".git"));
217
218 let test_file = inner.join("main.rs");
219 let _ = std::fs::write(&test_file, "fn main() {}");
220
221 let root = detect_project_root(test_file.to_str().unwrap());
222 assert!(root.is_some(), "should find a project root for nested .git");
223 let root_path = std::path::PathBuf::from(root.unwrap());
224 assert_eq!(
225 crate::core::pathutil::safe_canonicalize(&root_path).ok(),
226 crate::core::pathutil::safe_canonicalize(&base).ok(),
227 "should return outermost .git, not inner"
228 );
229
230 let _ = std::fs::remove_dir_all(&base);
231 }
232
233 #[test]
234 fn decoder_block_contains_all_codes() {
235 let block = instruction_decoder_block();
236 for t in TEMPLATES {
237 assert!(
238 block.contains(t.code),
239 "decoder should contain code {}",
240 t.code
241 );
242 }
243 }
244
245 #[test]
246 fn encoded_instructions_are_compact() {
247 use super::super::tokens::count_tokens;
248 let full = "TASK COMPLEXITY: mechanical\nMinimal reasoning needed. Act immediately, report result in one line. Show only changed lines, not full files.";
249 let encoded = encode_instructions("mechanical");
250 assert!(
251 count_tokens(&encoded) <= count_tokens(full),
252 "encoded ({}) should be <= full ({})",
253 count_tokens(&encoded),
254 count_tokens(full)
255 );
256 }
257
258 #[test]
259 fn all_complexity_levels_encode() {
260 for level in &["mechanical", "standard", "architectural"] {
261 let encoded = encode_instructions(level);
262 assert!(encoded.starts_with("MODE:"), "should start with MODE:");
263 }
264 }
265}