Skip to main content

xchecker_config/config/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use xchecker_selectors::Selectors;
5use xchecker_utils::types::ConfigSource;
6
7/// Default timeout for hook execution in seconds
8pub const DEFAULT_HOOK_TIMEOUT_SECS: u64 = 60;
9
10/// Hook failure behavior
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum OnFail {
14    /// Log warning and continue (default)
15    #[default]
16    Warn,
17    /// Fail the phase on hook failure
18    Fail,
19}
20
21impl std::fmt::Display for OnFail {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            Self::Warn => write!(f, "warn"),
25            Self::Fail => write!(f, "fail"),
26        }
27    }
28}
29
30/// Hook type indicating when the hook runs
31/// Reserved for hooks integration; not wired in v1.0
32#[cfg_attr(not(test), allow(dead_code))]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HookType {
35    /// Runs before phase execution
36    PrePhase,
37    /// Runs after phase execution
38    PostPhase,
39}
40
41impl std::fmt::Display for HookType {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Self::PrePhase => write!(f, "pre_phase"),
45            Self::PostPhase => write!(f, "post_phase"),
46        }
47    }
48}
49
50/// Configuration for a single hook
51#[derive(Debug, Clone, Deserialize, Serialize)]
52pub struct HookConfig {
53    /// Command to execute (can be a script path or shell command)
54    pub command: String,
55    /// Behavior on hook failure (default: warn)
56    #[serde(default)]
57    pub on_fail: OnFail,
58    /// Timeout in seconds (default: 60)
59    #[serde(default = "default_timeout")]
60    pub timeout: u64,
61}
62
63fn default_timeout() -> u64 {
64    DEFAULT_HOOK_TIMEOUT_SECS
65}
66
67/// Hooks configuration section from config.toml
68#[derive(Debug, Clone, Default, Deserialize, Serialize)]
69pub struct HooksConfig {
70    /// Pre-phase hooks keyed by phase name
71    #[serde(default)]
72    pub pre_phase: HashMap<String, HookConfig>,
73    /// Post-phase hooks keyed by phase name
74    #[serde(default)]
75    pub post_phase: HashMap<String, HookConfig>,
76}
77
78impl HooksConfig {
79    /// Get a pre-phase hook for the given phase
80    /// Reserved for hooks integration; not wired in v1.0
81    #[must_use]
82    #[cfg_attr(not(test), allow(dead_code))]
83    pub fn get_pre_phase_hook(&self, phase: crate::types::PhaseId) -> Option<&HookConfig> {
84        self.pre_phase.get(phase.as_str())
85    }
86
87    /// Get a post-phase hook for the given phase
88    /// Reserved for hooks integration; not wired in v1.0
89    #[must_use]
90    #[cfg_attr(not(test), allow(dead_code))]
91    pub fn get_post_phase_hook(&self, phase: crate::types::PhaseId) -> Option<&HookConfig> {
92        self.post_phase.get(phase.as_str())
93    }
94
95    /// Check if any hooks are configured
96    /// Reserved for hooks integration; not wired in v1.0
97    #[must_use]
98    #[cfg_attr(not(test), allow(dead_code))]
99    pub fn has_hooks(&self) -> bool {
100        !self.pre_phase.is_empty() || !self.post_phase.is_empty()
101    }
102}
103
104/// Configuration for xchecker operations.
105///
106/// `Config` provides hierarchical configuration with discovery and precedence:
107/// CLI arguments > config file > built-in defaults.
108///
109/// # Discovery
110///
111/// Use [`Config::discover()`] for CLI-like behavior that:
112/// - Searches for `.xchecker/config.toml` upward from current directory
113/// - Respects the `XCHECKER_HOME` environment variable
114/// - Applies built-in defaults for unspecified values
115///
116/// # Programmatic Configuration
117///
118/// For embedding scenarios where you need deterministic behavior independent
119/// of the user's environment, construct a `Config` directly or use
120/// `xchecker::OrchestratorHandle::from_config()`.
121///
122/// # Source Attribution
123///
124/// Each configuration value tracks its source (`cli`, `config`, `programmatic`, or `default`)
125/// for debugging and status display.
126///
127/// # Example
128///
129/// ```rust,no_run
130/// use xchecker_config::Config;
131/// use xchecker_config::CliArgs;
132///
133/// // Discover configuration using CLI semantics
134/// let config = Config::discover(&CliArgs::default())?;
135///
136/// // Access configuration values
137/// println!("Model: {:?}", config.defaults.model);
138/// println!("Max turns: {:?}", config.defaults.max_turns);
139/// # Ok::<(), Box<dyn std::error::Error>>(())
140/// ```
141///
142/// # Configuration File Format
143///
144/// Configuration files use TOML format with these sections:
145///
146/// ```toml
147/// [defaults]
148/// model = "haiku"
149/// max_turns = 6
150/// phase_timeout = 600
151///
152/// [selectors]
153/// include = ["**/*.md", "**/*.yaml"]
154/// exclude = ["target/**", "node_modules/**"]
155///
156/// [runner]
157/// mode = "auto"
158///
159/// [llm]
160/// provider = "claude-cli"
161/// ```
162#[derive(Debug, Clone)]
163pub struct Config {
164    /// Default values for various settings.
165    pub defaults: Defaults,
166    /// File selection patterns for packet building.
167    pub selectors: Selectors,
168    /// Runner configuration for cross-platform execution.
169    pub runner: RunnerConfig,
170    /// LLM provider configuration.
171    pub llm: LlmConfig,
172    /// Per-phase configuration overrides.
173    pub phases: PhasesConfig,
174    /// Hooks configuration for pre/post phase scripts.
175    // Reserved for hooks integration; not wired in v1.0
176    #[allow(dead_code)]
177    pub hooks: HooksConfig,
178    /// Security configuration for secret detection and redaction.
179    pub security: SecurityConfig,
180    /// Source attribution for each setting (for status display).
181    pub source_attribution: HashMap<String, ConfigSource>,
182}
183
184/// Default configuration values
185///
186/// # Model selection
187///
188/// - **Testing/Development**: Leave `model` unset to use `haiku` (fast, cost-effective)
189/// - **Production**: Set `model = "sonnet"` or `model = "default"` for best results
190/// - **Complex tasks**: Set `model = "opus"` for maximum capability
191///
192/// Specific model versions (e.g., `claude-sonnet-4-5-20250929`) can be used for
193/// reproducibility but simple aliases are recommended.
194#[derive(Debug, Clone, Deserialize, Serialize)]
195pub struct Defaults {
196    /// Model to use. Default: haiku (for testing). Use "sonnet" or "default" for production.
197    pub model: Option<String>,
198    pub max_turns: Option<u32>,
199    pub packet_max_bytes: Option<usize>,
200    pub packet_max_lines: Option<usize>,
201    pub output_format: Option<String>,
202    pub verbose: Option<bool>,
203    pub phase_timeout: Option<u64>,
204    pub stdout_cap_bytes: Option<usize>,
205    pub stderr_cap_bytes: Option<usize>,
206    pub lock_ttl_seconds: Option<u64>,
207    pub debug_packet: Option<bool>,
208    pub allow_links: Option<bool>,
209    /// Enable strict validation for phase outputs.
210    ///
211    /// When enabled, validation failures (meta-summaries, too-short output,
212    /// missing required sections) become hard errors that fail the phase.
213    /// When disabled (default), validation issues are logged as warnings only.
214    pub strict_validation: Option<bool>,
215}
216
217/// LLM provider configuration
218#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct LlmConfig {
220    pub provider: Option<String>,
221    pub fallback_provider: Option<String>,
222    pub claude: Option<ClaudeConfig>,
223    pub gemini: Option<GeminiConfig>,
224    pub openrouter: Option<OpenRouterConfig>,
225    pub anthropic: Option<AnthropicConfig>,
226    pub execution_strategy: Option<String>,
227    /// Prompt template to use for LLM interactions
228    ///
229    /// Available templates:
230    /// - "default": Universal template compatible with all providers
231    /// - "claude-optimized": Optimized for Claude CLI and Anthropic API
232    /// - "openai-compatible": Optimized for OpenRouter and OpenAI-compatible APIs
233    ///
234    /// If not specified, defaults to "default" which works with all providers.
235    pub prompt_template: Option<String>,
236}
237
238/// Claude CLI provider configuration
239#[derive(Debug, Clone, Deserialize, Serialize)]
240pub struct ClaudeConfig {
241    pub binary: Option<String>,
242}
243
244/// Gemini CLI provider configuration
245#[derive(Debug, Clone, Deserialize, Serialize)]
246pub struct GeminiConfig {
247    pub binary: Option<String>,
248    pub default_model: Option<String>,
249    pub profiles: Option<HashMap<String, GeminiProfileConfig>>,
250}
251
252/// Gemini profile configuration for per-phase model selection
253#[derive(Debug, Clone, Deserialize, Serialize)]
254pub struct GeminiProfileConfig {
255    pub model: Option<String>,
256    pub max_tokens: Option<u32>,
257}
258
259/// OpenRouter HTTP provider configuration
260#[derive(Debug, Clone, Deserialize, Serialize)]
261pub struct OpenRouterConfig {
262    pub api_key_env: Option<String>,
263    pub base_url: Option<String>,
264    pub model: Option<String>,
265    pub max_tokens: Option<u32>,
266    pub temperature: Option<f32>,
267    pub budget: Option<u32>,
268}
269
270/// Anthropic HTTP provider configuration
271#[derive(Debug, Clone, Deserialize, Serialize)]
272pub struct AnthropicConfig {
273    pub api_key_env: Option<String>,
274    pub base_url: Option<String>,
275    pub model: Option<String>,
276    pub max_tokens: Option<u32>,
277    pub temperature: Option<f32>,
278}
279
280/// Per-phase configuration overrides
281///
282/// Allows configuring model, timeout, and max_turns on a per-phase basis.
283/// Values set here override global defaults for that specific phase.
284#[derive(Debug, Clone, Deserialize, Serialize, Default)]
285pub struct PhaseConfig {
286    /// Model to use for this phase (overrides defaults.model)
287    pub model: Option<String>,
288    /// Maximum turns for this phase (overrides defaults.max_turns)
289    pub max_turns: Option<u32>,
290    /// Phase timeout in seconds (overrides defaults.phase_timeout)
291    pub phase_timeout: Option<u64>,
292}
293
294/// Phase-specific configuration section
295///
296/// Contains optional per-phase configuration overrides.
297/// If a phase is not specified or None, global defaults are used.
298///
299/// # Example
300///
301/// ```toml
302/// [defaults]
303/// model = "haiku"
304///
305/// [phases.design]
306/// model = "sonnet"
307///
308/// [phases.tasks]
309/// model = "sonnet"
310/// ```
311#[derive(Debug, Clone, Deserialize, Serialize, Default)]
312pub struct PhasesConfig {
313    pub requirements: Option<PhaseConfig>,
314    pub design: Option<PhaseConfig>,
315    pub tasks: Option<PhaseConfig>,
316    pub review: Option<PhaseConfig>,
317    pub fixup: Option<PhaseConfig>,
318    #[serde(rename = "final")]
319    pub final_: Option<PhaseConfig>,
320}
321
322/// Runner configuration for cross-platform execution
323#[derive(Debug, Clone, Deserialize, Serialize)]
324pub struct RunnerConfig {
325    pub mode: Option<String>,
326    pub distro: Option<String>,
327    pub claude_path: Option<String>,
328}
329
330/// Security configuration for secret detection and redaction
331///
332/// This section allows customizing secret detection patterns:
333/// - Add extra patterns to detect project-specific secrets
334/// - Ignore patterns that cause false positives
335///
336/// # Example
337///
338/// ```toml
339/// [security]
340/// extra_secret_patterns = ["SECRET_[A-Z0-9]{32}", "API_KEY_[A-Za-z0-9]{40}"]
341/// ignore_secret_patterns = ["github_pat"]
342/// ```
343#[derive(Debug, Clone, Deserialize, Serialize, Default)]
344pub struct SecurityConfig {
345    /// Additional regex patterns for secret detection.
346    ///
347    /// These patterns are added to the built-in patterns and will cause
348    /// secret detection to trigger if matched.
349    #[serde(default)]
350    pub extra_secret_patterns: Vec<String>,
351
352    /// Patterns to suppress from secret detection.
353    ///
354    /// Pattern IDs listed here will be ignored during secret scanning.
355    /// Use this to suppress false positives for known-safe patterns.
356    ///
357    /// **Warning:** Suppressing patterns reduces security. Only suppress
358    /// patterns if you're certain they won't match real secrets.
359    #[serde(default)]
360    pub ignore_secret_patterns: Vec<String>,
361}
362
363impl Default for Defaults {
364    fn default() -> Self {
365        Self {
366            model: None,
367            max_turns: Some(6),
368            packet_max_bytes: Some(65536),
369            packet_max_lines: Some(1200),
370            output_format: Some("stream-json".to_string()),
371            verbose: Some(false),
372            phase_timeout: Some(600),        // 600 seconds = 10 minutes
373            stdout_cap_bytes: Some(2097152), // 2 MiB
374            stderr_cap_bytes: Some(262144),  // 256 KiB
375            lock_ttl_seconds: Some(900),     // 15 minutes
376            debug_packet: Some(false),
377            allow_links: Some(false),
378            strict_validation: None, // Default: soft validation (warnings only)
379        }
380    }
381}
382
383impl Default for RunnerConfig {
384    fn default() -> Self {
385        Self {
386            mode: Some("auto".to_string()),
387            distro: None,
388            claude_path: None,
389        }
390    }
391}