use std::path::Path;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CrpMode {
Off,
Compact,
Tdd,
}
impl CrpMode {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"off" => Some(Self::Off),
"compact" => Some(Self::Compact),
"tdd" => Some(Self::Tdd),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Compact => "compact",
Self::Tdd => "tdd",
}
}
pub fn resolve(profile_crp: Option<&str>) -> Self {
use crate::core::config::{CompressionLevel, Config};
let env = std::env::var("LEAN_CTX_CRP_MODE").ok();
let forced = CompressionLevel::session_degrade_level().or_else(CompressionLevel::from_env);
let configured = CompressionLevel::effective(&Config::load());
Self::resolve_from(env.as_deref(), forced, configured, profile_crp)
}
pub(crate) fn resolve_from(
env: Option<&str>,
forced: Option<crate::core::config::CompressionLevel>,
configured: crate::core::config::CompressionLevel,
profile_crp: Option<&str>,
) -> Self {
let level_crp = |level: crate::core::config::CompressionLevel| {
Self::parse(level.to_components().2).unwrap_or(Self::Off)
};
if let Some(v) = env.filter(|v| !v.trim().is_empty()) {
return Self::parse(v).unwrap_or(Self::Off);
}
if let Some(level) = forced {
return level_crp(level);
}
let configured = level_crp(configured);
if configured != Self::Off {
return configured;
}
profile_crp.and_then(Self::parse).unwrap_or(Self::Off)
}
pub fn effective() -> Self {
Self::resolve(
crate::core::profiles::active_profile()
.compression
.crp_mode
.as_deref(),
)
}
pub fn is_tdd(&self) -> bool {
*self == Self::Tdd
}
}
#[derive(Clone, Debug)]
pub struct ToolCallRecord {
pub tool: String,
pub original_tokens: usize,
pub saved_tokens: usize,
pub mode: Option<String>,
pub duration_ms: u64,
pub timestamp: String,
}
pub fn detect_project_root(file_path: &str) -> Option<String> {
let start = Path::new(file_path);
let mut dir = if start.is_dir() {
start
} else {
start.parent()?
};
let (mut git, mut workspace, mut generic): (Option<&Path>, Option<&Path>, Option<&Path>) =
(None, None, None);
loop {
if !crate::core::pathutil::is_broad_or_unsafe_root(dir) {
if dir.join(".git").exists() {
git = Some(dir);
} else if is_workspace_root(dir) {
workspace = Some(dir);
} else if generic.is_none() && is_project_root_marker(dir) {
generic = Some(dir);
}
}
match dir.parent() {
Some(parent) if parent != dir => dir = parent,
_ => break,
}
}
git.or(workspace)
.or(generic)
.map(|dir| dir.to_string_lossy().to_string())
}
fn is_workspace_root(dir: &Path) -> bool {
const WORKSPACE_MARKERS: &[&str] = &[
"go.work",
"pnpm-workspace.yaml",
"lerna.json",
"nx.json",
"turbo.json",
];
let declares = |file: &str, needle: &str| {
std::fs::read_to_string(dir.join(file)).is_ok_and(|text| text.contains(needle))
};
WORKSPACE_MARKERS.iter().any(|m| dir.join(m).exists())
|| declares("Cargo.toml", "[workspace]")
|| declares("package.json", "\"workspaces\"")
}
pub fn request_project_unresolved() -> bool {
crate::core::policy::runtime::REQUEST_PROJECT
.try_with(|slot| slot.borrow().is_none())
.unwrap_or(false)
}
fn is_project_root_marker(dir: &Path) -> bool {
const MARKERS: &[&str] = &[
".git",
"Cargo.toml",
"package.json",
"go.work",
"pnpm-workspace.yaml",
"lerna.json",
"nx.json",
"turbo.json",
".projectile",
"pyproject.toml",
"setup.py",
"Makefile",
"CMakeLists.txt",
"BUILD.bazel",
];
MARKERS.iter().any(|m| dir.join(m).exists())
}
pub fn detect_project_root_or_cwd(file_path: &str) -> String {
if let Ok(project) =
crate::core::policy::runtime::REQUEST_PROJECT.try_with(|slot| slot.borrow().clone())
{
return match project {
Some(root) => root.to_string_lossy().into_owned(),
None => detect_project_root(file_path).unwrap_or_else(|| {
let path = Path::new(file_path);
if path.is_dir() {
file_path.to_string()
} else {
path.parent().map_or_else(
|| file_path.to_string(),
|p| p.to_string_lossy().into_owned(),
)
}
}),
};
}
if let Ok(env_root) = std::env::var("LEAN_CTX_PROJECT_ROOT")
&& !env_root.is_empty()
{
return env_root;
}
let cfg = crate::core::config::Config::load();
if let Some(ref cfg_root) = cfg.project_root
&& !cfg_root.is_empty()
{
return cfg_root.clone();
}
if let Some(ide_root) = resolve_ide_path(&cfg, file_path) {
return ide_root;
}
if let Some(root) = detect_project_root(file_path) {
return root;
}
let fallback = {
let p = Path::new(file_path);
if p.exists() {
if p.is_dir() {
file_path.to_string()
} else {
p.parent().map_or_else(
|| file_path.to_string(),
|pp| pp.to_string_lossy().to_string(),
)
}
} else {
std::env::current_dir()
.map_or_else(|_| ".".to_string(), |p| p.to_string_lossy().to_string())
}
};
if is_broad_directory(&fallback) && !crate::core::runtime_flags::is_dashboard_server() {
use std::sync::Once;
static WARN_ONCE: Once = Once::new();
WARN_ONCE.call_once(|| {
tracing::warn!(
"[protocol: no project detected — current directory is {fallback} which is not a project root.\n \
To fix: run from inside a project (with .git, Cargo.toml, package.json, etc.)\n \
Or set: export LEAN_CTX_PROJECT_ROOT=/path/to/your/project]"
);
});
}
fallback
}
fn is_broad_directory(path: &str) -> bool {
if path == "/" || path == "\\" || path == "." {
return true;
}
if let Some(home) = dirs::home_dir() {
let home_str = home.to_string_lossy();
if path == home_str.as_ref() || path == format!("{home_str}/") {
return true;
}
}
false
}
fn resolve_ide_path(cfg: &crate::core::config::Config, file_path: &str) -> Option<String> {
if cfg.ide_paths.is_empty() {
return None;
}
let agent = std::env::var("LEAN_CTX_AGENT").ok()?;
let agent_lower = agent.to_lowercase();
let paths = cfg.ide_paths.get(&agent_lower)?;
let fp = Path::new(file_path);
for allowed in paths {
let ap = Path::new(allowed.as_str());
if fp.starts_with(ap) {
return Some(allowed.clone());
}
}
paths.first().cloned()
}
pub fn display_path(path: &str) -> String {
path.replace('\\', "/")
}
pub fn shorten_path(path: &str) -> String {
let normalized = display_path(path);
let p = Path::new(&normalized);
if let Some(name) = p.file_name() {
return name.to_string_lossy().to_string();
}
normalized
}
pub fn shorten_path_relative(path: &str, root: &str) -> String {
let norm_path = display_path(path);
let norm_root = display_path(root);
let norm_root = norm_root.strip_suffix('/').unwrap_or(&norm_root);
if let Some(rest) = norm_path.strip_prefix(norm_root)
&& let Some(rel) = rest.strip_prefix('/')
&& !rel.is_empty()
{
return rel.to_string();
}
shorten_path(&norm_path)
}
static MCP_CONTEXT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn set_mcp_context(active: bool) {
MCP_CONTEXT.store(active, std::sync::atomic::Ordering::Relaxed);
}
pub fn savings_footer_visible() -> bool {
if matches!(std::env::var("LEAN_CTX_QUIET"), Ok(v) if v.trim() == "1") {
return false;
}
if matches!(std::env::var("LEAN_CTX_SHOW_SAVINGS"), Ok(v) if v.trim() == "0") {
return false;
}
if matches!(std::env::var("LEAN_CTX_SHOW_SAVINGS"), Ok(v) if v.trim() == "1") {
return true;
}
let mode = super::config::SavingsFooter::effective();
match mode {
super::config::SavingsFooter::Always => true,
super::config::SavingsFooter::Never => false,
super::config::SavingsFooter::Auto => {
!MCP_CONTEXT.load(std::sync::atomic::Ordering::Relaxed)
}
}
}
pub fn meta_visible() -> bool {
if matches!(std::env::var("LEAN_CTX_QUIET"), Ok(v) if v.trim() == "1") {
return false;
}
matches!(std::env::var("LEAN_CTX_META"), Ok(v) if v.trim() == "1")
|| matches!(std::env::var("LEAN_CTX_DIAGNOSTICS"), Ok(v) if v.trim() == "1")
}
pub fn format_savings(original: usize, compressed: usize) -> String {
super::savings_footer::format_footer_basic(original, compressed)
}
pub fn format_savings_with_info(
original: usize,
compressed: usize,
mode: Option<&str>,
detail: Option<&str>,
) -> String {
super::savings_footer::format_footer(&super::savings_footer::SavingsInfo {
original,
compressed,
mode,
detail,
})
}
pub fn append_savings(output: &str, original: usize, compressed: usize) -> String {
super::savings_footer::append_footer_basic(output, original, compressed)
}
pub fn append_savings_with_info(
output: &str,
original: usize,
compressed: usize,
mode: Option<&str>,
detail: Option<&str>,
) -> String {
super::savings_footer::append_footer(
output,
&super::savings_footer::SavingsInfo {
original,
compressed,
mode,
detail,
},
)
}
pub fn strip_trailing_savings_footer(output: &str) -> &str {
let body = output.trim_end_matches('\n');
let (head, last_line) = match body.rfind('\n') {
Some(nl) => (&body[..nl], &body[nl + 1..]),
None => ("", body),
};
if is_savings_footer_line(last_line) {
head
} else {
output
}
}
fn is_savings_footer_line(line: &str) -> bool {
let l = line.trim();
(l.starts_with("\u{2500}\u{2500}\u{2500} ") && l.ends_with(" \u{2500}\u{2500}\u{2500}"))
|| (l.starts_with("[lean-ctx: ") && l.ends_with(']'))
}
pub struct InstructionTemplate {
pub code: &'static str,
pub full: &'static str,
}
const TEMPLATES: &[InstructionTemplate] = &[
InstructionTemplate {
code: "ACT1",
full: "act now, 1-line result",
},
InstructionTemplate {
code: "BRIEF",
full: "1-2 line approach, then act",
},
InstructionTemplate {
code: "FULL",
full: "outline+edge cases first",
},
InstructionTemplate {
code: "DELTA",
full: "changed lines only",
},
InstructionTemplate {
code: "NOREPEAT",
full: "use Fn refs",
},
InstructionTemplate {
code: "STRUCT",
full: "+/-/~",
},
InstructionTemplate {
code: "1LINE",
full: "1 line/action",
},
InstructionTemplate {
code: "QUALITY",
full: "keep edge cases",
},
InstructionTemplate {
code: "FREF",
full: "Fn refs, no paths",
},
InstructionTemplate {
code: "DIFF",
full: "diff lines only",
},
];
pub fn instruction_decoder_block(tdd_active: bool) -> String {
if !tdd_active {
return String::new();
}
let pairs: Vec<String> = TEMPLATES
.iter()
.map(|t| format!("{}={}", t.code, t.full))
.collect();
format!("INSTRUCTION CODES:\n {}", pairs.join(" | "))
}
pub fn encode_instructions(complexity: &str) -> String {
match complexity {
"mechanical" => "MODE: ACT1 DELTA 1LINE | BUDGET: <=50 tokens, 1 line answer".to_string(),
"simple" => "MODE: BRIEF DELTA 1LINE | BUDGET: <=100 tokens, structured".to_string(),
"standard" => "MODE: BRIEF DELTA NOREPEAT STRUCT | BUDGET: <=200 tokens".to_string(),
"complex" => {
"MODE: FULL QUALITY NOREPEAT STRUCT FREF DIFF | BUDGET: <=500 tokens".to_string()
}
"architectural" => {
"MODE: FULL QUALITY NOREPEAT STRUCT FREF | BUDGET: unlimited".to_string()
}
_ => "MODE: BRIEF | BUDGET: <=200 tokens".to_string(),
}
}
pub fn encode_instructions_with_snr(complexity: &str, compression_pct: f64) -> String {
let snr = if compression_pct > 0.0 {
1.0 - (compression_pct / 100.0)
} else {
1.0
};
let base = encode_instructions(complexity);
format!("{base} | SNR: {snr:.2}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_pin_does_not_replace_the_request_project() {
let _data = crate::core::data_dir::isolated_data_dir();
let (request, pinned) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let file = request.path().join("lib.rs");
std::fs::write(&file, "fn main() {}\n").unwrap();
let _pin = crate::test_env::VarGuard::set("LEAN_CTX_PROJECT_ROOT", pinned.path());
let root = crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
std::cell::RefCell::new(Some(request.path().to_path_buf())),
|| detect_project_root_or_cwd(&file.to_string_lossy()),
);
assert_eq!(root, request.path().to_string_lossy());
}
#[test]
fn strip_trailing_savings_footer_handles_both_styles() {
let boxed = "body line one\nbody line two\n\u{2500}\u{2500}\u{2500} 4,200 \u{2192} 840 tok (\u{2193}80%) \u{2500}\u{2500}\u{2500}";
assert_eq!(
strip_trailing_savings_footer(boxed),
"body line one\nbody line two"
);
let verbatim = "out\n[lean-ctx: 4200\u{2192}840 tok, verbatim truncated]";
assert_eq!(strip_trailing_savings_footer(verbatim), "out");
assert_eq!(
strip_trailing_savings_footer("plain body\n"),
"plain body\n"
);
assert_eq!(
strip_trailing_savings_footer("[lean-ctx: 10\u{2192}5 tok, verbatim truncated]"),
""
);
assert_eq!(
strip_trailing_savings_footer("see [lean-ctx: docs] for details"),
"see [lean-ctx: docs] for details"
);
}
#[test]
fn display_path_normalizes_windows_separators() {
assert_eq!(
display_path(r"C:\Users\zir\AppData\Local\Temp\win-build-log.txt"),
"C:/Users/zir/AppData/Local/Temp/win-build-log.txt"
);
assert_eq!(display_path("src/main.rs"), "src/main.rs");
}
#[test]
fn shorten_path_basename_for_windows_abs_path() {
assert_eq!(
shorten_path(r"D:\Temp\win-build-raw.log"),
"win-build-raw.log"
);
assert_eq!(shorten_path("a/b/c.txt"), "c.txt");
}
#[test]
fn shorten_path_relative_handles_windows_separators() {
assert_eq!(
shorten_path_relative(r"C:\proj\src\app\main.rs", r"C:\proj"),
"src/app/main.rs"
);
assert_eq!(
shorten_path_relative(r"C:\proj\src\main.rs", "C:/proj/"),
"src/main.rs"
);
assert_eq!(
shorten_path_relative(r"C:\Users\zir\Temp\build.log", r"D:\proj"),
"build.log"
);
}
#[test]
fn shorten_path_relative_requires_component_boundary() {
assert_eq!(shorten_path_relative("a/bc/d.rs", "a/b"), "d.rs");
assert_eq!(shorten_path_relative("a/b/d.rs", "a/b"), "d.rs");
}
#[test]
fn is_project_root_marker_detects_git() {
let tmp = std::env::temp_dir().join("lean-ctx-test-root-marker");
let _ = std::fs::create_dir_all(&tmp);
let git_dir = tmp.join(".git");
let _ = std::fs::create_dir_all(&git_dir);
assert!(is_project_root_marker(&tmp));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn is_project_root_marker_detects_cargo_toml() {
let tmp = std::env::temp_dir().join("lean-ctx-test-cargo-marker");
let _ = std::fs::create_dir_all(&tmp);
let _ = std::fs::write(tmp.join("Cargo.toml"), "[package]");
assert!(is_project_root_marker(&tmp));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn detect_project_root_finds_outermost() {
let base = std::env::temp_dir().join("lean-ctx-test-monorepo");
let inner = base.join("packages").join("app");
let _ = std::fs::create_dir_all(&inner);
let _ = std::fs::create_dir_all(base.join(".git"));
let _ = std::fs::create_dir_all(inner.join(".git"));
let test_file = inner.join("main.rs");
let _ = std::fs::write(&test_file, "fn main() {}");
let root = detect_project_root(test_file.to_str().unwrap());
assert!(root.is_some(), "should find a project root for nested .git");
let root_path = std::path::PathBuf::from(root.unwrap());
assert_eq!(
crate::core::pathutil::safe_canonicalize(&root_path).ok(),
crate::core::pathutil::safe_canonicalize(&base).ok(),
"should return outermost .git, not inner"
);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn stray_generic_marker_in_an_ancestor_is_not_the_project() {
let canon = |p: &std::path::Path| crate::core::pathutil::safe_canonicalize(p).ok();
let detect = |file: &std::path::Path| {
detect_project_root(file.to_str().unwrap()).map(std::path::PathBuf::from)
};
let base = tempfile::tempdir().unwrap();
std::fs::write(base.path().join("Makefile"), "all:\n").unwrap();
let repo = base.path().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(repo.join("src/main.rs"), "fn main() {}").unwrap();
let found = detect(&repo.join("src/main.rs")).unwrap();
assert_eq!(canon(&found), canon(&repo));
let plain = base.path().join("plain");
std::fs::create_dir_all(&plain).unwrap();
std::fs::write(plain.join("package.json"), "{}").unwrap();
std::fs::write(plain.join("index.js"), "").unwrap();
let found = detect(&plain.join("index.js")).unwrap();
assert_eq!(canon(&found), canon(&plain));
let ws = base.path().join("ws");
let member = ws.join("crates").join("a");
std::fs::create_dir_all(&member).unwrap();
std::fs::write(
ws.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n",
)
.unwrap();
std::fs::write(member.join("Cargo.toml"), "[package]\nname = \"a\"\n").unwrap();
std::fs::write(member.join("lib.rs"), "").unwrap();
let found = detect(&member.join("lib.rs")).unwrap();
assert_eq!(canon(&found), canon(&ws));
}
#[test]
fn decoder_block_contains_all_codes() {
let block = instruction_decoder_block(true);
for t in TEMPLATES {
assert!(
block.contains(t.code),
"decoder should contain code {}",
t.code
);
}
}
#[test]
fn decoder_block_empty_outside_tdd() {
{
let found = &instruction_decoder_block(false);
assert!(found.is_empty(), "{found:?}");
}
}
#[test]
fn decoder_codes_match_what_encode_can_emit() {
let all_modes: Vec<String> = [
"mechanical",
"simple",
"standard",
"complex",
"architectural",
"unknown",
]
.iter()
.map(|c| encode_instructions(c))
.collect();
for t in TEMPLATES {
assert!(
all_modes.iter().any(|m| m.contains(t.code)),
"code {} is defined but never emitted",
t.code
);
}
}
#[test]
fn encoded_instructions_are_compact() {
use super::super::tokens::count_tokens;
let full = "TASK COMPLEXITY: mechanical\nMinimal reasoning needed. Act immediately, report result in one line. Show only changed lines, not full files.";
let encoded = encode_instructions("mechanical");
assert!(
count_tokens(&encoded) <= count_tokens(full),
"encoded ({}) should be <= full ({})",
count_tokens(&encoded),
count_tokens(full)
);
}
#[test]
fn all_complexity_levels_encode() {
for level in &["mechanical", "standard", "architectural"] {
let encoded = encode_instructions(level);
assert!(encoded.starts_with("MODE:"), "should start with MODE:");
}
}
#[test]
fn savings_footer_env_gated_tests() {
let _lock = crate::core::data_dir::test_env_lock();
super::MCP_CONTEXT.store(false, std::sync::atomic::Ordering::Relaxed);
crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "always");
crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "1");
crate::test_env::set_var("LEAN_CTX_COMPRESSION_ANNOTATION", "full");
crate::test_env::remove_var("LEAN_CTX_QUIET");
let s = super::format_savings(100, 50);
assert!(s.contains("\u{2192}"), "expected arrow: {s}");
assert!(s.contains("\u{2193}50%"), "expected pct: {s}");
assert!(
s.starts_with("\u{2500}\u{2500}\u{2500}"),
"expected box-drawing: {s}"
);
let s = super::format_savings_with_info(4200, 840, Some("map"), None);
assert!(s.contains("mode: map"), "expected mode: {s}");
assert!(s.contains("\u{2193}80%"), "expected 80%: {s}");
crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "never");
crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "0");
let s = super::format_savings(100, 50);
assert!(s.is_empty(), "expected empty with never: {s}");
let result = super::append_savings("hello", 100, 50);
assert_eq!(result, "hello");
super::MCP_CONTEXT.store(true, std::sync::atomic::Ordering::Relaxed);
crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "auto");
crate::test_env::remove_var("LEAN_CTX_SHOW_SAVINGS");
let s = super::format_savings(100, 50);
assert!(s.is_empty(), "expected empty in MCP+auto: {s}");
super::MCP_CONTEXT.store(false, std::sync::atomic::Ordering::Relaxed);
crate::test_env::set_var("LEAN_CTX_SAVINGS_FOOTER", "never");
crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "1");
assert!(super::savings_footer_visible());
crate::test_env::set_var("LEAN_CTX_SHOW_SAVINGS", "0");
assert!(!super::savings_footer_visible());
crate::test_env::remove_var("LEAN_CTX_SHOW_SAVINGS");
crate::test_env::remove_var("LEAN_CTX_SAVINGS_FOOTER");
crate::test_env::remove_var("LEAN_CTX_COMPRESSION_ANNOTATION");
}
#[test]
fn crp_resolution_precedence() {
use crate::core::config::CompressionLevel as L;
let r = CrpMode::resolve_from;
assert_eq!(r(None, None, L::Lite, None), CrpMode::Off);
assert_eq!(r(None, None, L::Lite, Some("tdd")), CrpMode::Tdd);
assert_eq!(r(None, None, L::Off, Some("compact")), CrpMode::Compact);
assert_eq!(r(None, None, L::Standard, Some("tdd")), CrpMode::Compact);
assert_eq!(r(None, None, L::Max, Some("off")), CrpMode::Tdd);
assert_eq!(r(None, Some(L::Lite), L::Max, Some("tdd")), CrpMode::Off);
assert_eq!(
r(Some("tdd"), Some(L::Off), L::Off, Some("off")),
CrpMode::Tdd
);
assert_eq!(r(Some("bogus"), None, L::Max, Some("tdd")), CrpMode::Off);
assert_eq!(r(Some(" "), None, L::Lite, Some("tdd")), CrpMode::Tdd);
assert_eq!(r(None, None, L::Lite, Some("weird")), CrpMode::Off);
}
#[test]
fn crp_mode_str_round_trips() {
for m in [CrpMode::Off, CrpMode::Compact, CrpMode::Tdd] {
assert_eq!(CrpMode::parse(m.as_str()), Some(m));
}
}
}