sqlite_graphrag/config/settings.rs
1//! Read and write of operational settings (`config get/set/unset/list`).
2//!
3//! The precedence itself (flag > XDG > default) is applied by callers through
4//! [`crate::runtime_config`]; this module only owns the XDG layer.
5
6use super::registry::{is_known_setting, nearest_setting_key, LEGACY_SETTING_KEYS, SETTING_KEYS};
7use super::store::{load_config, save_config};
8use crate::errors::AppError;
9use crate::i18n::validation;
10
11/// Read an operational setting from XDG config (flag > XDG > default is
12/// applied by callers). Returns `None` when unset.
13pub fn get_setting(key: &str) -> Result<Option<String>, AppError> {
14 let cfg = load_config()?;
15 if let Some(v) = cfg.settings.get(key) {
16 return Ok(Some(v.clone()));
17 }
18 // GAP-SG-122: when the canonical key is missing, fall back to any retired
19 // alias that still maps onto it (e.g. paths.cache → cache.dir).
20 for (legacy, replacement) in LEGACY_SETTING_KEYS {
21 if *replacement == key {
22 if let Some(v) = cfg.settings.get(*legacy) {
23 return Ok(Some(v.clone()));
24 }
25 }
26 }
27 Ok(None)
28}
29
30/// Persist an operational setting in XDG config.toml (G-T-XDG-01).
31pub fn set_setting(key: &str, value: &str) -> Result<(), AppError> {
32 if key.trim().is_empty() {
33 return Err(AppError::Validation("config key must be non-empty".into()));
34 }
35 // GAP-SG-80: an unvalidated insert persisted typos and obsolete keys while
36 // reporting success, so the operator saw the value in `config show` and
37 // assumed it took effect. Reject early instead of storing a silent no-op.
38 if !is_known_setting(key) {
39 if let Some(replacement) = LEGACY_SETTING_KEYS
40 .iter()
41 .find(|(legacy, _)| *legacy == key)
42 .map(|(_, replacement)| *replacement)
43 {
44 return Err(AppError::Validation(validation::config_key_retired(
45 key,
46 replacement,
47 )));
48 }
49 return Err(AppError::Validation(validation::config_key_unknown(
50 key,
51 nearest_setting_key(key),
52 )));
53 }
54 // GAP-SG-201: the key was validated and the VALUE never was, so
55 // `embedding.dim nao-numero`, `log.format xml` and `llm.slot_no_wait talvez`
56 // all persisted with exit 0 and only misbehaved on a later invocation, far
57 // from the command that caused them. `display.tz 0` was the extreme case:
58 // it bricked every subsequent run, including the `config unset` that would
59 // have undone it (GAP-SG-200).
60 //
61 // Rejecting here puts the error where the operator can act on it. Keys of
62 // kind `Text` have no checkable domain and skip this by construction.
63 if let Some(entry) = SETTING_KEYS.iter().find(|entry| entry.key == key) {
64 if let Some(expectation) = entry.kind.expectation() {
65 if !entry.kind.accepts(value) {
66 return Err(AppError::Validation(validation::config_value_invalid(
67 key,
68 value,
69 &expectation,
70 )));
71 }
72 }
73 }
74 let mut cfg = load_config()?;
75 cfg.settings.insert(key.to_string(), value.to_string());
76 save_config(&cfg)
77}
78
79/// Remove an operational setting from XDG config.toml.
80pub fn unset_setting(key: &str) -> Result<bool, AppError> {
81 let mut cfg = load_config()?;
82 let removed = cfg.settings.remove(key).is_some();
83 if removed {
84 save_config(&cfg)?;
85 }
86 Ok(removed)
87}
88
89/// List all operational settings (no secrets).
90pub fn list_settings() -> Result<std::collections::BTreeMap<String, String>, AppError> {
91 let cfg = load_config()?;
92 Ok(cfg.settings)
93}