Skip to main content

chant/config/
defaults.rs

1//! Default values and configuration structs with default implementations.
2
3use serde::{Deserialize, Serialize};
4
5use crate::provider::ProviderType;
6
7/// Macro to generate default functions for serde attributes
8macro_rules! default_fn {
9    ($name:ident, $type:ty, $value:expr) => {
10        pub(crate) fn $name() -> $type {
11            $value
12        }
13    };
14}
15
16// =========================================================================
17// DEFAULT VALUE FUNCTIONS
18// =========================================================================
19
20default_fn!(default_complexity_criteria, usize, 10);
21default_fn!(default_complexity_files, usize, 5);
22default_fn!(default_complexity_words, usize, 150);
23default_fn!(default_simple_criteria, usize, 1);
24default_fn!(default_simple_files, usize, 1);
25default_fn!(default_simple_words, usize, 3);
26default_fn!(default_max_retries, usize, 3);
27default_fn!(default_retry_delay_ms, u64, 60_000); // 60 seconds
28default_fn!(default_backoff_multiplier, f64, 2.0);
29default_fn!(default_poll_interval_ms, u64, 5000); // 5 seconds
30default_fn!(default_site_output_dir, String, "./public/".to_string());
31default_fn!(default_site_base_url, String, "/".to_string());
32default_fn!(default_site_title, String, "Project Specs".to_string());
33default_fn!(
34    default_include_statuses,
35    Vec<String>,
36    vec![
37        "completed".to_string(),
38        "in_progress".to_string(),
39        "pending".to_string(),
40    ]
41);
42default_fn!(default_true, bool, true);
43default_fn!(default_agent_weight, usize, 1);
44default_fn!(default_agent_name, String, "main".to_string());
45default_fn!(default_agent_command, String, "claude".to_string());
46default_fn!(default_max_concurrent, usize, 2);
47default_fn!(default_stagger_delay_ms, u64, 1000); // Default 1 second between agent spawns
48default_fn!(default_stagger_jitter_ms, u64, 200); // Default 20% of stagger_delay_ms (200ms is 20% of 1000ms)
49default_fn!(default_rotation_strategy, String, "none".to_string());
50default_fn!(default_prompt, String, "bootstrap".to_string());
51default_fn!(default_branch_prefix, String, "chant/".to_string());
52default_fn!(default_main_branch, String, "main".to_string());
53
54// =========================================================================
55// CONFIG STRUCTS WITH DEFAULTS
56// =========================================================================
57
58/// Thresholds for linter complexity heuristics
59#[derive(Debug, Clone, Deserialize, Serialize)]
60pub struct LintThresholds {
61    /// Max acceptance criteria for complex specs (default: 10)
62    #[serde(default = "default_complexity_criteria")]
63    pub complexity_criteria: usize,
64    /// Max target files for complex specs (default: 5)
65    #[serde(default = "default_complexity_files")]
66    pub complexity_files: usize,
67    /// Max words in title for complex specs (default: 150)
68    #[serde(default = "default_complexity_words")]
69    pub complexity_words: usize,
70    /// Min acceptance criteria for simple specs (default: 1)
71    #[serde(default = "default_simple_criteria")]
72    pub simple_criteria: usize,
73    /// Min target files for simple specs (default: 1)
74    #[serde(default = "default_simple_files")]
75    pub simple_files: usize,
76    /// Min words in title for simple specs (default: 3)
77    #[serde(default = "default_simple_words")]
78    pub simple_words: usize,
79}
80
81impl Default for LintThresholds {
82    fn default() -> Self {
83        Self {
84            complexity_criteria: default_complexity_criteria(),
85            complexity_files: default_complexity_files(),
86            complexity_words: default_complexity_words(),
87            simple_criteria: default_simple_criteria(),
88            simple_files: default_simple_files(),
89            simple_words: default_simple_words(),
90        }
91    }
92}
93
94/// Default disabled lint rules
95fn default_lint_disable() -> Vec<String> {
96    vec!["complexity_words".to_string()]
97}
98
99/// Linter configuration for spec validation
100#[derive(Debug, Clone, Deserialize, Serialize)]
101pub struct LintConfig {
102    /// Thresholds for complexity heuristics
103    #[serde(default)]
104    pub thresholds: LintThresholds,
105    /// List of rule names to disable (skip during linting)
106    #[serde(default = "default_lint_disable")]
107    pub disable: Vec<String>,
108}
109
110impl Default for LintConfig {
111    fn default() -> Self {
112        Self {
113            thresholds: LintThresholds::default(),
114            disable: default_lint_disable(),
115        }
116    }
117}
118
119/// Failure handling strategy for permanent failures
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
121#[serde(rename_all = "lowercase")]
122pub enum OnPermanentFailure {
123    /// Skip the failed spec and continue watching others
124    #[default]
125    Skip,
126    /// Stop the watch command entirely
127    Stop,
128}
129
130/// Configuration for failure handling in watch command
131#[derive(Debug, Clone, Deserialize, Serialize)]
132pub struct FailureConfig {
133    /// Maximum number of retry attempts for transient errors
134    #[serde(default = "default_max_retries")]
135    pub max_retries: usize,
136    /// Delay in milliseconds before first retry
137    #[serde(default = "default_retry_delay_ms")]
138    pub retry_delay_ms: u64,
139    /// Multiplier for exponential backoff (must be >= 1.0)
140    #[serde(default = "default_backoff_multiplier")]
141    pub backoff_multiplier: f64,
142    /// Regex patterns for errors that should be retried
143    #[serde(default)]
144    pub retryable_patterns: Vec<String>,
145    /// Action to take on permanent failure
146    #[serde(default)]
147    pub on_permanent_failure: OnPermanentFailure,
148}
149
150impl Default for FailureConfig {
151    fn default() -> Self {
152        Self {
153            max_retries: default_max_retries(),
154            retry_delay_ms: default_retry_delay_ms(),
155            backoff_multiplier: default_backoff_multiplier(),
156            retryable_patterns: vec![],
157            on_permanent_failure: OnPermanentFailure::default(),
158        }
159    }
160}
161
162/// Configuration for watch command behavior
163#[derive(Debug, Clone, Deserialize, Serialize)]
164pub struct WatchConfig {
165    /// Poll interval in milliseconds
166    #[serde(default = "default_poll_interval_ms")]
167    pub poll_interval_ms: u64,
168    /// Failure handling configuration
169    #[serde(default)]
170    pub failure: FailureConfig,
171    /// Idle timeout in minutes (default: 5)
172    #[serde(default = "default_idle_timeout_minutes")]
173    pub idle_timeout_minutes: u64,
174}
175
176fn default_idle_timeout_minutes() -> u64 {
177    5
178}
179
180impl Default for WatchConfig {
181    fn default() -> Self {
182        Self {
183            poll_interval_ms: default_poll_interval_ms(),
184            failure: FailureConfig::default(),
185            idle_timeout_minutes: default_idle_timeout_minutes(),
186        }
187    }
188}
189
190/// Configuration for what specs to include in the site
191#[derive(Debug, Clone, Deserialize, Serialize)]
192pub struct SiteIncludeConfig {
193    /// Statuses to include (default: completed, in_progress, pending)
194    #[serde(default = "default_include_statuses")]
195    pub statuses: Vec<String>,
196    /// Labels to include (empty = all)
197    #[serde(default)]
198    pub labels: Vec<String>,
199}
200
201impl Default for SiteIncludeConfig {
202    fn default() -> Self {
203        Self {
204            statuses: default_include_statuses(),
205            labels: vec![],
206        }
207    }
208}
209
210/// Configuration for what to exclude from the site
211#[derive(Debug, Clone, Deserialize, Serialize, Default)]
212pub struct SiteExcludeConfig {
213    /// Labels to exclude from output
214    #[serde(default)]
215    pub labels: Vec<String>,
216    /// Fields to redact from output (e.g., cost_usd, tokens)
217    #[serde(default)]
218    pub fields: Vec<String>,
219}
220
221/// Feature toggles for site pages
222#[derive(Debug, Clone, Deserialize, Serialize)]
223pub struct SiteFeaturesConfig {
224    /// Generate changelog page
225    #[serde(default = "default_true")]
226    pub changelog: bool,
227    /// Generate dependency graph page
228    #[serde(default = "default_true")]
229    pub dependency_graph: bool,
230    /// Generate timeline page
231    #[serde(default = "default_true")]
232    pub timeline: bool,
233    /// Generate status index pages
234    #[serde(default = "default_true")]
235    pub status_indexes: bool,
236    /// Generate label index pages
237    #[serde(default = "default_true")]
238    pub label_indexes: bool,
239}
240
241impl Default for SiteFeaturesConfig {
242    fn default() -> Self {
243        Self {
244            changelog: true,
245            dependency_graph: true,
246            timeline: true,
247            status_indexes: true,
248            label_indexes: true,
249        }
250    }
251}
252
253/// Graph detail level for dependency visualization
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
255#[serde(rename_all = "lowercase")]
256pub enum GraphDetailLevel {
257    /// Show only spec IDs
258    Minimal,
259    /// Show IDs and titles
260    Titles,
261    /// Show IDs, titles, status, and labels
262    #[default]
263    Full,
264}
265
266/// Configuration for dependency graph visualization
267#[derive(Debug, Clone, Deserialize, Serialize)]
268pub struct SiteGraphConfig {
269    /// Level of detail in the graph
270    #[serde(default)]
271    pub detail: GraphDetailLevel,
272}
273
274impl Default for SiteGraphConfig {
275    fn default() -> Self {
276        Self {
277            detail: GraphDetailLevel::Full,
278        }
279    }
280}
281
282/// Timeline grouping option
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
284#[serde(rename_all = "lowercase")]
285pub enum TimelineGroupBy {
286    /// Group by day
287    #[default]
288    Day,
289    /// Group by week
290    Week,
291    /// Group by month
292    Month,
293}
294
295/// Configuration for timeline visualization
296#[derive(Debug, Clone, Deserialize, Serialize)]
297pub struct SiteTimelineConfig {
298    /// How to group timeline entries
299    #[serde(default)]
300    pub group_by: TimelineGroupBy,
301    /// Whether to include pending specs in timeline
302    #[serde(default)]
303    pub include_pending: bool,
304}
305
306impl Default for SiteTimelineConfig {
307    fn default() -> Self {
308        Self {
309            group_by: TimelineGroupBy::Day,
310            include_pending: false,
311        }
312    }
313}
314
315/// Configuration for static site generation
316#[derive(Debug, Clone, Deserialize, Serialize, Default)]
317pub struct SiteConfig {
318    /// Output directory for generated site (default: ./public/)
319    #[serde(default = "default_site_output_dir")]
320    pub output_dir: String,
321    /// Base URL for the site (default: /)
322    #[serde(default = "default_site_base_url")]
323    pub base_url: String,
324    /// Site title (default: "Project Specs")
325    #[serde(default = "default_site_title")]
326    pub title: String,
327    /// Content filtering - what to include
328    #[serde(default)]
329    pub include: SiteIncludeConfig,
330    /// Content filtering - what to exclude
331    #[serde(default)]
332    pub exclude: SiteExcludeConfig,
333    /// Feature toggles for different page types
334    #[serde(default)]
335    pub features: SiteFeaturesConfig,
336    /// Graph visualization options
337    #[serde(default)]
338    pub graph: SiteGraphConfig,
339    /// Timeline visualization options
340    #[serde(default)]
341    pub timeline: SiteTimelineConfig,
342}
343
344/// Configuration for a single agent (Claude account/command)
345#[derive(Debug, Deserialize, Clone)]
346pub struct AgentConfig {
347    /// Name of the agent (for display and attribution)
348    #[serde(default = "default_agent_name")]
349    pub name: String,
350    /// Shell command to invoke this agent (e.g., "claude", "claude-alt1")
351    #[serde(default = "default_agent_command")]
352    pub command: String,
353    /// Maximum concurrent instances for this agent
354    #[serde(default = "default_max_concurrent")]
355    pub max_concurrent: usize,
356    /// Weight for agent rotation selection (higher = more likely to be selected)
357    #[serde(default = "default_agent_weight")]
358    pub weight: usize,
359}
360
361impl Default for AgentConfig {
362    fn default() -> Self {
363        Self {
364            name: default_agent_name(),
365            command: default_agent_command(),
366            max_concurrent: default_max_concurrent(),
367            weight: default_agent_weight(),
368        }
369    }
370}
371
372/// Configuration for parallel execution with multiple agents
373#[derive(Debug, Deserialize, Clone)]
374pub struct ParallelConfig {
375    /// List of available agents (Claude accounts/commands)
376    #[serde(default)]
377    pub agents: Vec<AgentConfig>,
378    /// Delay in milliseconds between spawning each agent to avoid API rate limiting
379    #[serde(default = "default_stagger_delay_ms")]
380    pub stagger_delay_ms: u64,
381    /// Jitter in milliseconds for spawn delays (default: 20% of stagger_delay_ms)
382    #[serde(default = "default_stagger_jitter_ms")]
383    pub stagger_jitter_ms: u64,
384}
385
386impl ParallelConfig {
387    /// Calculate total capacity as sum of all agent max_concurrent values
388    pub fn total_capacity(&self) -> usize {
389        self.agents.iter().map(|a| a.max_concurrent).sum()
390    }
391}
392
393impl Default for ParallelConfig {
394    fn default() -> Self {
395        Self {
396            agents: vec![AgentConfig::default()],
397            stagger_delay_ms: default_stagger_delay_ms(),
398            stagger_jitter_ms: default_stagger_jitter_ms(),
399        }
400    }
401}
402
403#[derive(Debug, Clone, Deserialize)]
404pub struct DefaultsConfig {
405    #[serde(default = "default_prompt")]
406    pub prompt: String,
407    #[serde(default = "default_branch_prefix")]
408    pub branch_prefix: String,
409    /// Default model name to use when env vars are not set
410    #[serde(default)]
411    pub model: Option<String>,
412    /// Default model name for split operations (defaults to sonnet)
413    #[serde(default)]
414    pub split_model: Option<String>,
415    /// Default main branch name for merges (defaults to "main")
416    #[serde(default = "default_main_branch")]
417    pub main_branch: String,
418    /// Default provider (claude, ollama, openai)
419    #[serde(default)]
420    pub provider: ProviderType,
421    /// Agent rotation strategy for single spec execution (none, random, round-robin)
422    #[serde(default = "default_rotation_strategy")]
423    pub rotation_strategy: String,
424    /// List of prompt extensions to append to all prompts
425    #[serde(default)]
426    pub prompt_extensions: Vec<String>,
427}
428
429impl Default for DefaultsConfig {
430    fn default() -> Self {
431        Self {
432            prompt: default_prompt(),
433            branch_prefix: default_branch_prefix(),
434            model: None,
435            split_model: None,
436            main_branch: default_main_branch(),
437            provider: ProviderType::Claude,
438            rotation_strategy: default_rotation_strategy(),
439            prompt_extensions: vec![],
440        }
441    }
442}