Skip to main content

lean_agent_core/
config.rs

1//! Configuration objects for tracing and reporting.
2
3use crate::{Error, Result};
4use camino::{Utf8Path, Utf8PathBuf};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// User-configurable tracing settings.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct TraceConfig {
11    /// Root of the Lean/Lake workspace.
12    pub lake_root: Utf8PathBuf,
13    /// Whether directories should be searched recursively.
14    pub recursive: bool,
15    /// Process timeout per Lean file.
16    #[serde(with = "duration_seconds")]
17    pub timeout: Duration,
18    /// Keep raw stdout/stderr in trace records.
19    pub keep_raw_output: bool,
20    /// Include warnings in parsed diagnostics.
21    pub include_warnings: bool,
22    /// Include passing files in JSONL output.
23    pub include_passes: bool,
24    /// Substrings; a discovered file is skipped if its path contains any of them.
25    #[serde(default)]
26    pub exclude: Vec<String>,
27}
28
29impl TraceConfig {
30    /// Construct a default trace config rooted at the current working directory.
31    #[must_use]
32    pub fn new(lake_root: Utf8PathBuf) -> Self {
33        Self {
34            lake_root,
35            recursive: false,
36            timeout: Duration::from_secs(60),
37            keep_raw_output: false,
38            include_warnings: true,
39            include_passes: true,
40            exclude: Vec::new(),
41        }
42    }
43
44    /// Set recursive directory traversal.
45    #[must_use]
46    pub const fn recursive(mut self, recursive: bool) -> Self {
47        self.recursive = recursive;
48        self
49    }
50
51    /// Set process timeout.
52    #[must_use]
53    pub const fn timeout(mut self, timeout: Duration) -> Self {
54        self.timeout = timeout;
55        self
56    }
57
58    /// Set raw-output retention.
59    #[must_use]
60    pub const fn keep_raw_output(mut self, keep_raw_output: bool) -> Self {
61        self.keep_raw_output = keep_raw_output;
62        self
63    }
64}
65
66/// Report settings.
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub struct ReportConfig {
69    /// Number of sample diagnostics to include.
70    pub sample_limit: usize,
71}
72
73impl Default for ReportConfig {
74    fn default() -> Self {
75        Self { sample_limit: 10 }
76    }
77}
78
79/// On-disk `lean-agent.toml` schema.
80///
81/// All fields are optional so a partial file is valid; the CLI fills gaps with
82/// flag values and then hardcoded defaults.
83#[derive(Clone, Debug, Default, Deserialize, Serialize)]
84#[serde(deny_unknown_fields)]
85pub struct FileConfig {
86    /// `[project]` table.
87    #[serde(default)]
88    pub project: ProjectConfig,
89    /// `[trace]` table.
90    #[serde(default)]
91    pub trace: TraceFileConfig,
92}
93
94/// `[project]` settings from `lean-agent.toml`.
95#[derive(Clone, Debug, Default, Deserialize, Serialize)]
96#[serde(deny_unknown_fields)]
97pub struct ProjectConfig {
98    /// Human-readable project name, used only in logs.
99    pub name: Option<String>,
100    /// Lake workspace root.
101    pub lake_root: Option<Utf8PathBuf>,
102    /// Default trace targets when no path is given on the command line.
103    #[serde(default)]
104    pub source_roots: Vec<Utf8PathBuf>,
105    /// Path substrings to skip during discovery.
106    #[serde(default)]
107    pub exclude: Vec<String>,
108}
109
110/// `[trace]` settings from `lean-agent.toml`.
111#[derive(Clone, Debug, Default, Deserialize, Serialize)]
112#[serde(deny_unknown_fields)]
113pub struct TraceFileConfig {
114    /// Per-file process timeout in seconds.
115    pub timeout_secs: Option<u64>,
116    /// Keep raw stdout/stderr in records.
117    pub keep_raw_output: Option<bool>,
118    /// Include warnings in parsed diagnostics.
119    pub include_warnings: Option<bool>,
120    /// Emit only non-passing files.
121    pub only_failures: Option<bool>,
122}
123
124impl FileConfig {
125    /// Load and parse a `lean-agent.toml` file.
126    pub fn load(path: &Utf8Path) -> Result<Self> {
127        let text = std::fs::read_to_string(path).map_err(|source| Error::ConfigRead {
128            path: path.to_path_buf(),
129            source,
130        })?;
131        toml::from_str(&text).map_err(|source| Error::ConfigParse {
132            path: path.to_path_buf(),
133            source,
134        })
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn parse(text: &str) -> Result<FileConfig> {
143        toml::from_str(text).map_err(|source| Error::ConfigParse {
144            path: Utf8PathBuf::from("<test>"),
145            source,
146        })
147    }
148
149    #[test]
150    fn parses_full_config() -> Result<()> {
151        let config = parse(
152            r#"
153[project]
154name = "demo"
155lake_root = "."
156source_roots = ["src", "test"]
157exclude = [".lake/"]
158
159[trace]
160timeout_secs = 45
161keep_raw_output = true
162include_warnings = false
163only_failures = true
164"#,
165        )?;
166        assert_eq!(config.project.name.as_deref(), Some("demo"));
167        assert_eq!(config.project.source_roots.len(), 2);
168        assert_eq!(config.project.exclude, vec![".lake/".to_owned()]);
169        assert_eq!(config.trace.timeout_secs, Some(45));
170        assert_eq!(config.trace.keep_raw_output, Some(true));
171        assert_eq!(config.trace.include_warnings, Some(false));
172        assert_eq!(config.trace.only_failures, Some(true));
173        Ok(())
174    }
175
176    #[test]
177    fn empty_config_is_all_defaults() -> Result<()> {
178        let config = parse("")?;
179        assert!(config.project.name.is_none());
180        assert!(config.project.source_roots.is_empty());
181        assert!(config.trace.timeout_secs.is_none());
182        Ok(())
183    }
184
185    #[test]
186    fn unknown_field_is_rejected() {
187        assert!(parse("[trace]\nbogus = 1\n").is_err());
188    }
189}
190
191mod duration_seconds {
192    use serde::{Deserialize, Deserializer, Serializer};
193    use std::time::Duration;
194
195    pub(crate) fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
196    where
197        S: Serializer,
198    {
199        serializer.serialize_u64(duration.as_secs())
200    }
201
202    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
203    where
204        D: Deserializer<'de>,
205    {
206        let seconds = u64::deserialize(deserializer)?;
207        Ok(Duration::from_secs(seconds))
208    }
209}