1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::{Deserialize, Serialize};
5
6#[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 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 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}