Skip to main content

solverforge_config/
solver_config.rs

1use std::num::NonZeroUsize;
2use std::path::Path;
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::director::DirectorConfig;
8use crate::error::ConfigError;
9use crate::phase::PhaseConfig;
10use crate::termination::TerminationConfig;
11
12// Main solver configuration.
13#[derive(Debug, Clone, Default, Deserialize, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub struct SolverConfig {
16    // Environment mode affecting reproducibility and assertions.
17    #[serde(default)]
18    pub environment_mode: EnvironmentMode,
19
20    // Random seed for reproducible results.
21    #[serde(default)]
22    pub random_seed: Option<u64>,
23
24    // Number of threads for parallel move evaluation.
25    #[serde(default)]
26    pub move_thread_count: MoveThreadCount,
27
28    // Termination configuration.
29    #[serde(default)]
30    pub termination: Option<TerminationConfig>,
31
32    // Score director configuration.
33    #[serde(default)]
34    pub score_director: Option<DirectorConfig>,
35
36    // Phase configurations.
37    #[serde(default)]
38    pub phases: Vec<PhaseConfig>,
39
40    /// Optional bounded candidate-pull trace for cross-runtime diagnostics.
41    ///
42    /// This is deliberately opt-in: recording a trace retains one owned
43    /// identity per engine-consumed candidate.  A non-zero cap is required so
44    /// diagnostic runs cannot accidentally retain an unbounded neighborhood.
45    #[serde(default)]
46    pub candidate_trace: Option<CandidateTraceConfig>,
47}
48
49impl SolverConfig {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Loads configuration from a TOML file.
55    ///
56    /// # Errors
57    ///
58    /// Returns error if file doesn't exist or contains invalid TOML.
59    pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
60        Self::from_toml_file(path)
61    }
62
63    /// Loads configuration from a TOML file.
64    pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
65        let contents = std::fs::read_to_string(path)?;
66        Self::from_toml_str(&contents)
67    }
68
69    /// Parses configuration from a TOML string.
70    pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
71        Ok(toml::from_str(s)?)
72    }
73
74    /// Loads configuration from a YAML file.
75    pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
76        let contents = std::fs::read_to_string(path)?;
77        Self::from_yaml_str(&contents)
78    }
79
80    /// Parses configuration from a YAML string.
81    pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
82        Ok(serde_yaml::from_str(s)?)
83    }
84
85    pub fn with_termination_seconds(mut self, seconds: u64) -> Self {
86        self.termination = Some(TerminationConfig {
87            seconds_spent_limit: Some(seconds),
88            ..self.termination.unwrap_or_default()
89        });
90        self
91    }
92
93    pub fn with_random_seed(mut self, seed: u64) -> Self {
94        self.random_seed = Some(seed);
95        self
96    }
97
98    pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
99        self.phases.push(phase);
100        self
101    }
102
103    /// Returns a deterministic, complete representation of this effective
104    /// configuration for diagnostic provenance.
105    ///
106    /// The representation is intentionally produced by the same serde model
107    /// used to load solver configuration.  Consumers compare this exact value
108    /// together with the resolved phase plan in the candidate trace header.
109    pub fn canonical_toml(&self) -> String {
110        toml::to_string(self)
111            .expect("SolverConfig serialization must succeed for a serde-derived configuration")
112    }
113
114    /// Returns the canonical solver-document representation of one phase.
115    ///
116    /// Runtime provenance uses this rather than debug formatting so every
117    /// configured selector, acceptor, forager, target, and termination field
118    /// participates in the candidate-trace plan digest.
119    pub fn canonical_phase_toml(phase: &PhaseConfig) -> String {
120        Self {
121            phases: vec![phase.clone()],
122            ..Self::default()
123        }
124        .canonical_toml()
125    }
126
127    /// Returns the termination time limit, if configured.
128    ///
129    /// Convenience method that delegates to `termination.time_limit()`.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use solverforge_config::SolverConfig;
135    /// use std::time::Duration;
136    ///
137    /// let config = SolverConfig::from_toml_str(r#"
138    ///     [termination]
139    ///     seconds_spent_limit = 30
140    /// "#).unwrap();
141    ///
142    /// assert_eq!(config.time_limit(), Some(Duration::from_secs(30)));
143    /// ```
144    pub fn time_limit(&self) -> Option<Duration> {
145        self.termination.as_ref().and_then(|t| t.time_limit())
146    }
147}
148
149/// Opt-in bounded candidate-pull trace configuration.
150///
151/// Enable from TOML with, for example:
152///
153/// ```toml
154/// [candidate_trace]
155/// max_entries = 100_000
156/// ```
157///
158/// A zero cap is rejected during deserialization.  The trace records its
159/// total pull count even after the cap, marks itself truncated, and never
160/// retains identities beyond this limit.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
162#[serde(rename_all = "snake_case")]
163pub struct CandidateTraceConfig {
164    pub max_entries: NonZeroUsize,
165}
166
167impl CandidateTraceConfig {
168    pub const fn new(max_entries: NonZeroUsize) -> Self {
169        Self { max_entries }
170    }
171}
172
173// Environment mode affecting solver behavior.
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
175#[serde(rename_all = "snake_case")]
176pub enum EnvironmentMode {
177    // Non-reproducible mode with minimal overhead.
178    #[default]
179    NonReproducible,
180
181    // Reproducible mode with deterministic behavior.
182    Reproducible,
183
184    // Fast assert mode with basic assertions.
185    FastAssert,
186
187    // Full assert mode with comprehensive assertions.
188    FullAssert,
189}
190
191// Move thread count configuration.
192#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
193#[serde(rename_all = "snake_case")]
194pub enum MoveThreadCount {
195    // Automatically determine thread count.
196    #[default]
197    Auto,
198
199    // No parallel move evaluation.
200    None,
201
202    // Specific number of threads.
203    Count(usize),
204}
205
206// Runtime configuration overrides.
207#[derive(Debug, Clone, Default)]
208pub struct SolverConfigOverride {
209    // Override termination configuration.
210    pub termination: Option<TerminationConfig>,
211}
212
213impl SolverConfigOverride {
214    pub fn with_termination(termination: TerminationConfig) -> Self {
215        SolverConfigOverride {
216            termination: Some(termination),
217        }
218    }
219}