Skip to main content

ought_spec/
config.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6/// Project-level configuration from `ought.toml`.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    pub project: ProjectConfig,
10    #[serde(default)]
11    pub specs: SpecsConfig,
12    #[serde(default)]
13    pub context: ContextConfig,
14    pub generator: GeneratorConfig,
15    #[serde(default)]
16    pub runner: HashMap<String, RunnerConfig>,
17    #[serde(default)]
18    pub mcp: McpConfig,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ProjectConfig {
23    pub name: String,
24    #[serde(default = "default_version")]
25    pub version: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SpecsConfig {
30    #[serde(default = "default_roots")]
31    pub roots: Vec<PathBuf>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ContextConfig {
36    #[serde(default)]
37    pub search_paths: Vec<PathBuf>,
38    #[serde(default)]
39    pub exclude: Vec<String>,
40    #[serde(default = "default_max_files")]
41    pub max_files: usize,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GeneratorConfig {
46    pub provider: String,
47    #[serde(default)]
48    pub model: Option<String>,
49    #[serde(default)]
50    pub tolerance: ToleranceConfig,
51    #[serde(default = "default_parallelism")]
52    pub parallelism: usize,
53}
54
55impl Default for GeneratorConfig {
56    fn default() -> Self {
57        Self {
58            provider: "claude".to_string(),
59            model: None,
60            tolerance: ToleranceConfig::default(),
61            parallelism: default_parallelism(),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ToleranceConfig {
68    #[serde(default = "default_multiplier")]
69    pub must_by_multiplier: f64,
70}
71
72impl Default for ToleranceConfig {
73    fn default() -> Self {
74        Self {
75            must_by_multiplier: default_multiplier(),
76        }
77    }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct RunnerConfig {
82    pub command: String,
83    pub test_dir: PathBuf,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct McpConfig {
88    #[serde(default)]
89    pub enabled: bool,
90    #[serde(default = "default_transport")]
91    pub transport: String,
92}
93
94impl Config {
95    /// Load config from an `ought.toml` file.
96    pub fn load(path: &Path) -> anyhow::Result<Self> {
97        let content = std::fs::read_to_string(path)
98            .map_err(|e| anyhow::anyhow!("failed to read {}: {}", path.display(), e))?;
99        let config: Config = toml::from_str(&content)
100            .map_err(|e| anyhow::anyhow!("failed to parse {}: {}", path.display(), e))?;
101        Ok(config)
102    }
103
104    /// Discover `ought.toml` by walking up from the current directory.
105    pub fn discover() -> anyhow::Result<(PathBuf, Self)> {
106        let mut dir = std::env::current_dir()?;
107        loop {
108            let candidate = dir.join("ought.toml");
109            if candidate.is_file() {
110                let config = Self::load(&candidate)?;
111                return Ok((candidate, config));
112            }
113            if !dir.pop() {
114                anyhow::bail!("could not find ought.toml in any parent directory");
115            }
116        }
117    }
118}
119
120impl Default for SpecsConfig {
121    fn default() -> Self {
122        Self {
123            roots: default_roots(),
124        }
125    }
126}
127
128impl Default for ContextConfig {
129    fn default() -> Self {
130        Self {
131            search_paths: vec![],
132            exclude: vec![],
133            max_files: default_max_files(),
134        }
135    }
136}
137
138impl Default for McpConfig {
139    fn default() -> Self {
140        Self {
141            enabled: false,
142            transport: default_transport(),
143        }
144    }
145}
146
147fn default_version() -> String {
148    "0.1.0".into()
149}
150fn default_roots() -> Vec<PathBuf> {
151    vec![PathBuf::from("ought/")]
152}
153fn default_max_files() -> usize {
154    50
155}
156fn default_multiplier() -> f64 {
157    1.0
158}
159fn default_transport() -> String {
160    "stdio".into()
161}
162fn default_parallelism() -> usize {
163    1
164}