selfware/evolution/mod.rs
1//! # Selfware Evolution Engine
2//!
3//! Recursive self-improvement through evolutionary mutation, compilation-gated
4//! verification, parallel sandboxed evaluation, and SAB-driven fitness selection.
5//!
6//! ## Architecture
7//!
8//! ```text
9//! ┌─────────────┐
10//! │ Telemetry │ ◄── criterion + flamegraph
11//! └──────┬──────┘
12//! │ gradient signal
13//! ▼
14//! ┌──────────┐ ┌─────────────┐ ┌─────────────┐
15//! │ AST Tools│◄───│ Daemon │───►│ Sandbox │
16//! │ (mutate) │ │ (evolve) │ │ (evaluate) │
17//! └────┬─────┘ └──────┬──────┘ └──────┬──────┘
18//! │ │ │
19//! ▼ ▼ ▼
20//! ┌──────────┐ ┌─────────────┐ ┌─────────────┐
21//! │ cargo │ │ Fitness │ │ Tournament │
22//! │ check │ │ (Meta-SAB) │ │ (selection) │
23//! └──────────┘ └─────────────┘ └─────────────┘
24//! ```
25//!
26//! ## Safety Invariants
27//!
28//! 1. The evolution engine CANNOT modify its own fitness function
29//! 2. The evolution engine CANNOT modify the SAB benchmark suite
30//! 3. The evolution engine CANNOT modify the safety module
31//! 4. All mutations must pass `cargo check` before entering evaluation
32//! 5. Property tests are mandatory for core module mutations
33
34pub mod ast_tools;
35pub mod daemon;
36pub mod fitness;
37pub mod micro_mode;
38pub mod sandbox;
39pub mod telemetry;
40pub mod tournament;
41
42use std::path::PathBuf;
43
44/// Files that the evolution engine is NEVER allowed to modify.
45/// This is the cardinal safety invariant — the fitness landscape
46/// must be externally defined and immutable from the agent's perspective.
47pub const PROTECTED_PATHS: &[&str] = &[
48 "src/evolution/",
49 "src/safety/",
50 "system_tests/",
51 "benches/sab_",
52 // The fitness signal lives here: a mutation that can edit tests can
53 // weaken the very gate that judges it. Immutable from the agent.
54 "tests/",
55];
56
57/// LLM endpoint configuration for hypothesis generation
58#[derive(Clone)]
59pub struct LlmConfig {
60 /// API endpoint (e.g. `"https://api.example.com/v1"`)
61 pub endpoint: String,
62 /// Model identifier (e.g. "Qwen/Qwen3-Coder-Next-FP8")
63 pub model: String,
64 /// API key for authentication
65 pub api_key: Option<String>,
66 /// Max response tokens (default 16384)
67 pub max_tokens: usize,
68 /// Sampling temperature (default 0.7)
69 pub temperature: f32,
70}
71
72impl std::fmt::Debug for LlmConfig {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 // Never print credentials — dry-run and log output include this struct.
75 f.debug_struct("LlmConfig")
76 .field("endpoint", &self.endpoint)
77 .field("model", &self.model)
78 .field(
79 "api_key",
80 &self.api_key.as_ref().map(|k| {
81 let mut m: String = k.chars().take(4).collect();
82 m.push_str("…[redacted]");
83 m
84 }),
85 )
86 .field("max_tokens", &self.max_tokens)
87 .field("temperature", &self.temperature)
88 .finish()
89 }
90}
91
92impl Default for LlmConfig {
93 fn default() -> Self {
94 Self {
95 endpoint: String::from("http://localhost:8080/v1"),
96 model: String::from("default"),
97 api_key: None,
98 max_tokens: 16384,
99 temperature: 0.7,
100 }
101 }
102}
103
104/// Configuration for the evolution daemon, typically loaded from selfware.toml
105#[derive(Debug, Clone)]
106pub struct EvolutionConfig {
107 /// Number of generations to run (0 = infinite)
108 pub generations: usize,
109 /// Number of hypotheses generated per generation
110 pub population_size: usize,
111 /// Maximum concurrent Docker sandboxes
112 pub parallel_eval: usize,
113 /// Git tag checkpoint interval (every N generations)
114 pub checkpoint_interval: usize,
115 /// Fitness function weights
116 pub fitness_weights: FitnessWeights,
117 /// What the agent is allowed to mutate
118 pub mutation_targets: MutationTargets,
119 /// Safety constraints
120 pub safety: SafetyConfig,
121 /// LLM configuration for hypothesis generation
122 pub llm: LlmConfig,
123}
124
125#[derive(Debug, Clone)]
126pub struct FitnessWeights {
127 /// Weight for SAB benchmark aggregate score (0-100)
128 pub sab_score: f64,
129 /// Weight for token efficiency (lower tokens = better)
130 pub token_efficiency: f64,
131 /// Weight for wall-clock execution time
132 pub latency: f64,
133 /// Weight for maintaining/improving test coverage
134 pub test_coverage: f64,
135 /// Weight for preventing binary bloat
136 pub binary_size: f64,
137 /// Weight for visual quality (Visual-SAB scenarios).
138 /// Default 0.0 — set > 0 once visual scenarios are active.
139 pub visual_quality: f64,
140}
141
142impl FitnessWeights {
143 /// Compute composite fitness score from raw metrics
144 pub fn composite(&self, metrics: &FitnessMetrics) -> f64 {
145 let normalized_tokens =
146 1.0 - (metrics.tokens_used as f64 / metrics.token_budget as f64).min(1.0);
147 let normalized_latency = 1.0 - (metrics.wall_clock_secs / metrics.timeout_secs).min(1.0);
148 let normalized_coverage = metrics.test_coverage_pct / 100.0;
149 let normalized_size = 1.0 - (metrics.binary_size_mb / metrics.max_binary_size_mb).min(1.0);
150
151 let normalized_visual = metrics.visual_score / 100.0;
152
153 self.sab_score * (metrics.sab_score / 100.0)
154 + self.token_efficiency * normalized_tokens
155 + self.latency * normalized_latency
156 + self.test_coverage * normalized_coverage
157 + self.binary_size * normalized_size
158 + self.visual_quality * normalized_visual
159 }
160}
161
162impl Default for FitnessWeights {
163 fn default() -> Self {
164 Self {
165 sab_score: 0.50,
166 token_efficiency: 0.25,
167 latency: 0.15,
168 test_coverage: 0.05,
169 binary_size: 0.05,
170 // Default 0.0 — visual quality is opt-in until visual
171 // scenarios exist. Weights still sum to 1.0.
172 visual_quality: 0.0,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
178pub struct FitnessMetrics {
179 pub sab_score: f64,
180 pub tokens_used: u64,
181 pub token_budget: u64,
182 pub wall_clock_secs: f64,
183 pub timeout_secs: f64,
184 pub test_coverage_pct: f64,
185 pub binary_size_mb: f64,
186 pub max_binary_size_mb: f64,
187 pub tests_passed: usize,
188 pub tests_total: usize,
189 /// Average visual quality score from Visual-SAB scenarios (0–100).
190 pub visual_score: f64,
191}
192
193#[derive(Debug, Clone)]
194pub struct MutationTargets {
195 /// Config keys the agent can modify (e.g., temperature, token_budget)
196 pub config_keys: Vec<String>,
197 /// Source files containing prompt construction logic
198 pub prompt_logic: Vec<PathBuf>,
199 /// Source files containing tool implementations
200 pub tool_code: Vec<PathBuf>,
201 /// Source files containing cognitive architecture
202 pub cognitive: Vec<PathBuf>,
203}
204
205#[derive(Debug, Clone)]
206pub struct SafetyConfig {
207 /// Files that cannot be modified under any circumstances
208 pub protected_files: Vec<String>,
209 /// Minimum number of passing tests (prevents test deletion)
210 pub min_test_count: usize,
211 /// Maximum binary size in MB (prevents bloat)
212 pub max_binary_size_mb: f64,
213 /// If true, any test failure triggers immediate rollback
214 pub rollback_on_any_test_failure: bool,
215}
216
217impl Default for SafetyConfig {
218 fn default() -> Self {
219 Self {
220 protected_files: PROTECTED_PATHS.iter().map(|s| s.to_string()).collect(),
221 min_test_count: 5000,
222 max_binary_size_mb: 50.0,
223 rollback_on_any_test_failure: true,
224 }
225 }
226}
227
228/// Rating for a generation's outcome, using the garden aesthetic
229#[derive(Debug, Clone, Copy, PartialEq)]
230pub enum GenerationRating {
231 /// Score >= baseline + improvement_threshold
232 Bloom,
233 /// Score >= baseline (no regression, marginal improvement)
234 Grow,
235 /// Score < baseline but within tolerance
236 Wilt,
237 /// Score significantly below baseline or compilation failure
238 Frost,
239}
240
241impl std::fmt::Display for GenerationRating {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 Self::Bloom => write!(f, "BLOOM 🌸"),
245 Self::Grow => write!(f, "GROW 🌿"),
246 Self::Wilt => write!(f, "WILT 🥀"),
247 Self::Frost => write!(f, "FROST ❄️"),
248 }
249 }
250}
251
252/// Check if a path is protected from evolution mutations
253///
254/// Uses proper path prefix matching with canonicalization to prevent bypasses
255/// via symlinks, relative paths, or substring tricks.
256pub fn is_protected(path: &std::path::Path) -> bool {
257 // Canonicalize the path to resolve symlinks and normalize separators.
258 // We use the safety-checker normalize_path so the Windows `\\?\` UNC
259 // prefix is stripped — without that, `path_str.contains("src/evolution/")`
260 // would never match an extended-length canonicalized path.
261 let canonical_path = crate::safety::checker::normalize_path(path);
262
263 // PROTECTED_PATHS uses forward slashes; convert any `\` to `/` so the
264 // contains/starts_with checks work on Windows too.
265 let path_str = canonical_path.to_string_lossy().replace('\\', "/");
266
267 PROTECTED_PATHS.iter().any(|protected_prefix| {
268 // Check if the path starts with the protected prefix (for relative paths)
269 // or contains the protected prefix (for canonical/absolute paths)
270 // This handles both cases: "src/evolution/daemon.rs" and "/home/user/project/src/evolution/daemon.rs"
271 path_str.starts_with(protected_prefix) || path_str.contains(protected_prefix)
272 })
273}
274
275#[cfg(test)]
276#[path = "../../tests/unit/evolution/mod_test.rs"]
277mod tests;