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