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