lean_ctx/core/config/
provenance.rs1use std::path::PathBuf;
21
22use super::Config;
23
24const LOCAL_OVERRIDABLE_KEYS: &[&str] = &["compression_level", "terse_agent", "tool_profile"];
28
29const ENV_OVERRIDABLE: &[(&str, &str)] = &[
33 ("compression_level", "LEAN_CTX_COMPRESSION"),
34 ("terse_agent", "LEAN_CTX_TERSE_AGENT"),
35 ("tool_profile", "LEAN_CTX_TOOL_PROFILE"),
36 ("structure_first", "LEAN_CTX_STRUCTURE_FIRST"),
37];
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct EnvOverride {
42 pub setting: &'static str,
44 pub var: &'static str,
46}
47
48#[derive(Debug, Clone)]
51pub struct ConfigProvenance {
52 pub config_path: Option<PathBuf>,
55 pub config_exists: bool,
57 pub xdg_pinned: bool,
59 pub parse_error: Option<String>,
62 pub local_path: Option<PathBuf>,
64 pub local_exists: bool,
66 pub local_keys: Vec<&'static str>,
69 pub env_overrides: Vec<EnvOverride>,
71}
72
73impl ConfigProvenance {
74 #[must_use]
77 pub fn has_shadow(&self) -> bool {
78 !self.env_overrides.is_empty() || !self.local_keys.is_empty() || self.parse_error.is_some()
79 }
80
81 #[must_use]
85 #[allow(clippy::manual_contains)]
86 pub fn local_overrides(&self, setting: &str) -> bool {
87 self.local_keys.iter().any(|k| *k == setting)
88 }
89}
90
91impl Config {
92 #[must_use]
99 pub fn provenance() -> ConfigProvenance {
100 let config_path = Self::path();
101 let config_exists = config_path.as_ref().is_some_and(|p| p.exists());
102
103 let parse_error = config_path.as_ref().and_then(|p| {
107 let raw = std::fs::read_to_string(p).ok()?;
108 toml::from_str::<Config>(&raw).err().map(|e| e.to_string())
109 });
110
111 let local_path = Self::find_project_root().map(|r| Self::local_path(&r));
112 let local_content = local_path
113 .as_ref()
114 .filter(|p| crate::core::pathutil::may_probe_path(p.as_path()))
115 .and_then(|p| std::fs::read_to_string(p).ok());
116 let local_exists = local_content.is_some();
117 let local_keys = local_content
118 .as_deref()
119 .map(local_override_keys)
120 .unwrap_or_default();
121
122 let env_overrides = ENV_OVERRIDABLE
123 .iter()
124 .filter(|(_, var)| env_is_set(var))
125 .map(|&(setting, var)| EnvOverride { setting, var })
126 .collect();
127
128 ConfigProvenance {
129 config_path,
130 config_exists,
131 xdg_pinned: crate::core::layout_pin::is_xdg_pinned(),
132 parse_error,
133 local_path,
134 local_exists,
135 local_keys,
136 env_overrides,
137 }
138 }
139}
140
141fn local_override_keys(local_toml: &str) -> Vec<&'static str> {
145 let Ok(table) = local_toml.parse::<toml::Table>() else {
146 return Vec::new();
147 };
148 LOCAL_OVERRIDABLE_KEYS
149 .iter()
150 .filter(|k| table.contains_key(**k))
151 .copied()
152 .collect()
153}
154
155fn env_is_set(var: &str) -> bool {
158 std::env::var_os(var).is_some_and(|v| !v.is_empty())
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn local_override_keys_detects_editable_keys() {
167 let toml =
168 "compression_level = \"max\"\nterse_agent = \"ultra\"\ntool_profile = \"power\"\n";
169 let keys = local_override_keys(toml);
170 assert!(keys.contains(&"compression_level"));
171 assert!(keys.contains(&"terse_agent"));
172 assert!(keys.contains(&"tool_profile"));
173 }
174
175 #[test]
176 fn local_override_keys_ignores_structure_first_and_unrelated() {
177 let toml = "structure_first = true\nultra_compact = true\n";
179 assert!(local_override_keys(toml).is_empty());
180 }
181
182 #[test]
183 fn local_override_keys_ignores_comment_mentions() {
184 let toml = "# compression_level = \"max\" was considered\nultra_compact = false\n";
186 assert!(local_override_keys(toml).is_empty());
187 }
188
189 #[test]
190 fn local_override_keys_empty_on_parse_error() {
191 assert!(local_override_keys("this is = = not toml").is_empty());
192 }
193
194 #[test]
195 fn has_shadow_reflects_each_source() {
196 let clean = ConfigProvenance {
197 config_path: None,
198 config_exists: false,
199 xdg_pinned: true,
200 parse_error: None,
201 local_path: None,
202 local_exists: false,
203 local_keys: vec![],
204 env_overrides: vec![],
205 };
206 assert!(!clean.has_shadow());
207
208 let with_local = ConfigProvenance {
209 local_keys: vec!["compression_level"],
210 ..clean.clone()
211 };
212 assert!(with_local.has_shadow());
213 assert!(with_local.local_overrides("compression_level"));
214 assert!(!with_local.local_overrides("terse_agent"));
215
216 let with_env = ConfigProvenance {
217 env_overrides: vec![EnvOverride {
218 setting: "compression_level",
219 var: "LEAN_CTX_COMPRESSION",
220 }],
221 ..clean.clone()
222 };
223 assert!(with_env.has_shadow());
224
225 let with_parse_err = ConfigProvenance {
226 parse_error: Some("bad".into()),
227 ..clean
228 };
229 assert!(with_parse_err.has_shadow());
230 }
231
232 #[test]
233 fn provenance_reports_active_env_override() {
234 let _guard = crate::core::data_dir::test_env_lock();
235 crate::test_env::set_var("LEAN_CTX_COMPRESSION", "lite");
236 let prov = Config::provenance();
237 crate::test_env::remove_var("LEAN_CTX_COMPRESSION");
238
239 assert!(
240 prov.env_overrides
241 .iter()
242 .any(|e| e.setting == "compression_level" && e.var == "LEAN_CTX_COMPRESSION"),
243 "expected LEAN_CTX_COMPRESSION to be reported as an env override"
244 );
245 }
246}