Skip to main content

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;