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 {
45 detect_project_root(file_path).unwrap_or_else(|| {
46 std::env::current_dir()
47 .map(|p| p.to_string_lossy().to_string())
48 .unwrap_or_else(|_| ".".to_string())
49 })
50}
51
52pub fn shorten_path(path: &str) -> String {
53 let p = Path::new(path);
54 if let Some(name) = p.file_name() {
55 return name.to_string_lossy().to_string();
56 }
57 path.to_string()
58}
59
60pub fn format_savings(original: usize, compressed: usize) -> String {
61 let saved = original.saturating_sub(compressed);
62 if original == 0 {
63 return "0 tok saved".to_string();
64 }
65 let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
66 format!("[{saved} tok saved ({pct}%)]")
67}
68
69pub struct InstructionTemplate {
70 pub code: &'static str,
71 pub full: &'static str,
72}
73
74const TEMPLATES: &[InstructionTemplate] = &[
75 InstructionTemplate {
76 code: "ACT1",
77 full: "Act immediately, report result in one line",
78 },
79 InstructionTemplate {
80 code: "BRIEF",
81 full: "Summarize approach in 1-2 lines, then act",
82 },
83 InstructionTemplate {
84 code: "FULL",
85 full: "Outline approach, consider edge cases, then act",
86 },
87 InstructionTemplate {
88 code: "DELTA",
89 full: "Only show changed lines, not full files",
90 },
91 InstructionTemplate {
92 code: "NOREPEAT",
93 full: "Never repeat known context. Reference cached files by Fn ID",
94 },
95 InstructionTemplate {
96 code: "STRUCT",
97 full: "Use notation, not sentences. Changes: +line/-line/~line",
98 },
99 InstructionTemplate {
100 code: "1LINE",
101 full: "One line per action. Summarize, don't explain",
102 },
103 InstructionTemplate {
104 code: "NODOC",
105 full: "Don't add comments that narrate what code does",
106 },
107 InstructionTemplate {
108 code: "ACTFIRST",
109 full: "Execute tool calls immediately. Never narrate before acting",
110 },
111 InstructionTemplate {
112 code: "QUALITY",
113 full: "Never skip edge case analysis or error handling to save tokens",
114 },
115 InstructionTemplate {
116 code: "NOMOCK",
117 full: "Never use mock data, fake values, or placeholder code",
118 },
119 InstructionTemplate {
120 code: "FREF",
121 full: "Reference files by Fn refs only, never full paths",
122 },
123 InstructionTemplate {
124 code: "DIFF",
125 full: "For code changes: show only diff lines, not full files",
126 },
127 InstructionTemplate {
128 code: "ABBREV",
129 full: "Use abbreviations: fn, cfg, impl, deps, req, res, ctx, err",
130 },
131 InstructionTemplate {
132 code: "SYMBOLS",
133 full: "Use TDD notation: +=add -=remove ~=modify ->=returns ok/fail for status",
134 },
135];
136
137pub fn instruction_decoder_block() -> String {
139 let mut lines = vec!["INSTRUCTION CODES:".to_string()];
140 for t in TEMPLATES {
141 lines.push(format!(" {} = {}", t.code, t.full));
142 }
143 lines.join("\n")
144}
145
146pub fn encode_instructions(complexity: &str) -> String {
149 match complexity {
150 "mechanical" => "MODE: ACT1 DELTA 1LINE | BUDGET: <=50 tokens, 1 line answer".to_string(),
151 "simple" => "MODE: BRIEF DELTA 1LINE | BUDGET: <=100 tokens, structured".to_string(),
152 "standard" => "MODE: BRIEF DELTA NOREPEAT STRUCT | BUDGET: <=200 tokens".to_string(),
153 "complex" => {
154 "MODE: FULL QUALITY NOREPEAT STRUCT FREF DIFF | BUDGET: <=500 tokens".to_string()
155 }
156 "architectural" => {
157 "MODE: FULL QUALITY NOREPEAT STRUCT FREF | BUDGET: unlimited".to_string()
158 }
159 _ => "MODE: BRIEF | BUDGET: <=200 tokens".to_string(),
160 }
161}
162
163pub fn encode_instructions_with_snr(complexity: &str, compression_pct: f64) -> String {
165 let snr = if compression_pct > 0.0 {
166 1.0 - (compression_pct / 100.0)
167 } else {
168 1.0
169 };
170 let base = encode_instructions(complexity);
171 format!("{base} | SNR: {snr:.2}")
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn is_project_root_marker_detects_git() {
180 let tmp = std::env::temp_dir().join("lean-ctx-test-root-marker");
181 let _ = std::fs::create_dir_all(&tmp);
182 let git_dir = tmp.join(".git");
183 let _ = std::fs::create_dir_all(&git_dir);
184 assert!(is_project_root_marker(&tmp));
185 let _ = std::fs::remove_dir_all(&tmp);
186 }
187
188 #[test]
189 fn is_project_root_marker_detects_cargo_toml() {
190 let tmp = std::env::temp_dir().join("lean-ctx-test-cargo-marker");
191 let _ = std::fs::create_dir_all(&tmp);
192 let _ = std::fs::write(tmp.join("Cargo.toml"), "[package]");
193 assert!(is_project_root_marker(&tmp));
194 let _ = std::fs::remove_dir_all(&tmp);
195 }
196
197 #[test]
198 fn detect_project_root_finds_outermost() {
199 let base = std::env::temp_dir().join("lean-ctx-test-monorepo");
200 let inner = base.join("packages").join("app");
201 let _ = std::fs::create_dir_all(&inner);
202 let _ = std::fs::create_dir_all(base.join(".git"));
203 let _ = std::fs::create_dir_all(inner.join(".git"));
204
205 let test_file = inner.join("main.rs");
206 let _ = std::fs::write(&test_file, "fn main() {}");
207
208 let root = detect_project_root(test_file.to_str().unwrap());
209 assert!(root.is_some(), "should find a project root for nested .git");
210 let root_path = std::path::PathBuf::from(root.unwrap());
211 assert_eq!(
212 root_path.canonicalize().ok(),
213 base.canonicalize().ok(),
214 "should return outermost .git, not inner"
215 );
216
217 let _ = std::fs::remove_dir_all(&base);
218 }
219
220 #[test]
221 fn decoder_block_contains_all_codes() {
222 let block = instruction_decoder_block();
223 for t in TEMPLATES {
224 assert!(
225 block.contains(t.code),
226 "decoder should contain code {}",
227 t.code
228 );
229 }
230 }
231
232 #[test]
233 fn encoded_instructions_are_compact() {
234 use super::super::tokens::count_tokens;
235 let full = "TASK COMPLEXITY: mechanical\nMinimal reasoning needed. Act immediately, report result in one line. Show only changed lines, not full files.";
236 let encoded = encode_instructions("mechanical");
237 assert!(
238 count_tokens(&encoded) <= count_tokens(full),
239 "encoded ({}) should be <= full ({})",
240 count_tokens(&encoded),
241 count_tokens(full)
242 );
243 }
244
245 #[test]
246 fn all_complexity_levels_encode() {
247 for level in &["mechanical", "standard", "architectural"] {
248 let encoded = encode_instructions(level);
249 assert!(encoded.starts_with("MODE:"), "should start with MODE:");
250 }
251 }
252}