Skip to main content

oris_evolution/
pipeline.rs

1//! Evolution Pipeline - Complete detect/select/mutate runtime pipeline.
2//!
3//! This module implements the full evolution loop as separate runtime stages:
4//! - Detect: Extract signals from task context
5//! - Select: Choose gene candidates based on signals
6//! - Mutate: Prepare mutation proposals
7//! - Execute: Run the mutation in sandbox
8//! - Validate: Verify mutation correctness
9//! - Evaluate: Assess mutation quality
10//! - Solidify: Create gene/capsule events
11//! - Reuse: Mark capsule as reusable
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use thiserror::Error;
18
19use crate::core::{GeneCandidate, PreparedMutation, Selector, SelectorInput};
20use crate::evolver::{EvolutionSignal, MutationProposal, MutationRiskLevel, ValidationResult};
21use crate::port::{
22    EvaluateInput, EvaluatePort, GeneStorePersistPort, SandboxPort, SignalExtractorInput,
23    SignalExtractorPort, ValidateInput, ValidatePort,
24};
25
26/// Pipeline configuration
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct EvolutionPipelineConfig {
29    /// Enable/disable specific stages
30    pub enable_detect: bool,
31    pub enable_select: bool,
32    pub enable_mutate: bool,
33    pub enable_execute: bool,
34    pub enable_validate: bool,
35    pub enable_evaluate: bool,
36    pub enable_solidify: bool,
37    pub enable_reuse: bool,
38
39    /// Stage timeout in seconds
40    pub detect_timeout_secs: u64,
41    pub select_timeout_secs: u64,
42    pub mutate_timeout_secs: u64,
43    pub execute_timeout_secs: u64,
44    pub validate_timeout_secs: u64,
45    pub evaluate_timeout_secs: u64,
46    pub solidify_timeout_secs: u64,
47    pub reuse_timeout_secs: u64,
48
49    /// Max candidates to select
50    pub max_candidates: usize,
51
52    /// Min signal confidence threshold
53    pub min_signal_confidence: f32,
54}
55
56impl Default for EvolutionPipelineConfig {
57    fn default() -> Self {
58        Self {
59            enable_detect: true,
60            enable_select: true,
61            enable_mutate: true,
62            enable_execute: true,
63            enable_validate: true,
64            enable_evaluate: true,
65            enable_solidify: true,
66            enable_reuse: true,
67            detect_timeout_secs: 30,
68            select_timeout_secs: 30,
69            mutate_timeout_secs: 60,
70            execute_timeout_secs: 300,
71            validate_timeout_secs: 60,
72            evaluate_timeout_secs: 30,
73            solidify_timeout_secs: 30,
74            reuse_timeout_secs: 30,
75            max_candidates: 10,
76            min_signal_confidence: 0.5,
77        }
78    }
79}
80
81/// Stage state
82#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
83pub enum PipelineStageState {
84    /// Stage not started
85    Pending,
86    /// Stage currently running
87    Running,
88    /// Stage completed successfully
89    Completed,
90    /// Stage failed with error
91    Failed(String),
92    /// Stage was skipped
93    Skipped(String),
94}
95
96/// Pipeline execution context (internal use, not serialized)
97pub struct PipelineContext {
98    /// Input task context
99    pub task_input: serde_json::Value,
100    /// Optional extractor input for the Detect stage.
101    /// When set and a `SignalExtractorPort` is injected into the pipeline,
102    /// the Detect stage will call the extractor to populate `signals`.
103    pub extractor_input: Option<SignalExtractorInput>,
104    /// Signals extracted in Detect phase
105    pub signals: Vec<EvolutionSignal>,
106    /// Gene candidates selected in Select phase
107    pub candidates: Vec<GeneCandidate>,
108    /// Mutation proposals prepared in Mutate phase
109    pub proposals: Vec<MutationProposal>,
110    /// Execution result
111    pub execution_result: Option<serde_json::Value>,
112    /// Validation result
113    pub validation_result: Option<ValidationResult>,
114    /// Evaluation result
115    pub evaluation_result: Option<EvaluationResult>,
116    /// Solidified genes
117    pub solidified_genes: Vec<String>,
118    /// Reused capsules
119    pub reused_capsules: Vec<String>,
120    /// Wall-clock duration recorded for each stage that ran.
121    pub stage_timings: HashMap<String, Duration>,
122}
123
124impl Default for PipelineContext {
125    fn default() -> Self {
126        Self {
127            task_input: serde_json::json!({}),
128            extractor_input: None,
129            signals: Vec::new(),
130            candidates: Vec::new(),
131            proposals: Vec::new(),
132            execution_result: None,
133            validation_result: None,
134            evaluation_result: None,
135            solidified_genes: Vec::new(),
136            reused_capsules: Vec::new(),
137            stage_timings: HashMap::new(),
138        }
139    }
140}
141
142/// Evaluation result
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct EvaluationResult {
145    pub score: f32,
146    pub improvements: Vec<String>,
147    pub regressions: Vec<String>,
148    pub recommendation: EvaluationRecommendation,
149}
150
151#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
152pub enum EvaluationRecommendation {
153    Accept,
154    Reject,
155    NeedsRevision,
156    RequiresHumanReview,
157}
158
159/// Pipeline execution result
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct PipelineResult {
162    /// Whether the pipeline completed successfully
163    pub success: bool,
164    /// Stage states
165    pub stage_states: Vec<StageState>,
166    /// Error message if failed
167    pub error: Option<String>,
168}
169
170/// Individual stage state
171#[derive(Clone, Debug, Serialize, Deserialize)]
172pub struct StageState {
173    pub stage_name: String,
174    pub state: PipelineStageState,
175    pub duration_ms: Option<u64>,
176}
177
178impl StageState {
179    pub fn new(name: &str) -> Self {
180        Self {
181            stage_name: name.to_string(),
182            state: PipelineStageState::Pending,
183            duration_ms: None,
184        }
185    }
186}
187
188/// Pipeline errors
189#[derive(Error, Debug)]
190pub enum PipelineError {
191    #[error("Detect stage error: {0}")]
192    DetectError(String),
193
194    #[error("Select stage error: {0}")]
195    SelectError(String),
196
197    #[error("Mutate stage error: {0}")]
198    MutateError(String),
199
200    #[error("Execute stage error: {0}")]
201    ExecuteError(String),
202
203    #[error("Validate stage error: {0}")]
204    ValidateError(String),
205
206    #[error("Evaluate stage error: {0}")]
207    EvaluateError(String),
208
209    #[error("Solidify stage error: {0}")]
210    SolidifyError(String),
211
212    #[error("Reuse stage error: {0}")]
213    ReuseError(String),
214
215    #[error("Pipeline timeout: {0}")]
216    Timeout(String),
217}
218
219/// Evolution Pipeline trait
220pub trait EvolutionPipeline: Send + Sync {
221    /// Get pipeline name
222    fn name(&self) -> &str;
223
224    /// Get pipeline configuration
225    fn config(&self) -> &EvolutionPipelineConfig;
226
227    /// Execute the full pipeline
228    fn execute(&self, context: PipelineContext) -> Result<PipelineResult, PipelineError>;
229
230    /// Execute a specific stage
231    #[deprecated(note = "Use execute() instead")]
232    fn execute_stage(
233        &self,
234        stage: PipelineStage,
235        context: &mut PipelineContext,
236    ) -> Result<PipelineStageState, PipelineError>;
237}
238
239/// Pipeline stages
240#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
241pub enum PipelineStage {
242    Detect,
243    Select,
244    Mutate,
245    Execute,
246    Validate,
247    Evaluate,
248    Solidify,
249    Reuse,
250}
251
252impl PipelineStage {
253    pub fn as_str(&self) -> &'static str {
254        match self {
255            PipelineStage::Detect => "detect",
256            PipelineStage::Select => "select",
257            PipelineStage::Mutate => "mutate",
258            PipelineStage::Execute => "execute",
259            PipelineStage::Validate => "validate",
260            PipelineStage::Evaluate => "evaluate",
261            PipelineStage::Solidify => "solidify",
262            PipelineStage::Reuse => "reuse",
263        }
264    }
265
266    pub fn all() -> Vec<PipelineStage> {
267        vec![
268            PipelineStage::Detect,
269            PipelineStage::Select,
270            PipelineStage::Mutate,
271            PipelineStage::Execute,
272            PipelineStage::Validate,
273            PipelineStage::Evaluate,
274            PipelineStage::Solidify,
275            PipelineStage::Reuse,
276        ]
277    }
278}
279
280/// Standard evolution pipeline implementation
281pub struct StandardEvolutionPipeline {
282    name: String,
283    config: EvolutionPipelineConfig,
284    selector: Arc<dyn Selector>,
285    /// Optional signal extractor for the Detect stage.
286    signal_extractor: Option<Arc<dyn SignalExtractorPort>>,
287    /// Optional sandbox for the Execute stage.
288    sandbox: Option<Arc<dyn SandboxPort>>,
289    /// Optional gene store for the Solidify/Reuse stages.
290    gene_store: Option<Arc<dyn GeneStorePersistPort>>,
291    /// Optional validator for the Validate stage.
292    validate_port: Option<Arc<dyn ValidatePort>>,
293    /// Optional evaluator for the Evaluate stage.
294    evaluate_port: Option<Arc<dyn EvaluatePort>>,
295}
296
297impl StandardEvolutionPipeline {
298    /// Create a pipeline with a mandatory selector and no injected ports.
299    /// Detect will pass through pre-populated signals; Execute will use a
300    /// no-op stub that records a synthetic success result.
301    pub fn new(config: EvolutionPipelineConfig, selector: Arc<dyn Selector>) -> Self {
302        Self {
303            name: "standard".to_string(),
304            config,
305            selector,
306            signal_extractor: None,
307            sandbox: None,
308            gene_store: None,
309            validate_port: None,
310            evaluate_port: None,
311        }
312    }
313
314    /// Attach a `SignalExtractorPort` for the Detect stage.
315    pub fn with_signal_extractor(mut self, extractor: Arc<dyn SignalExtractorPort>) -> Self {
316        self.signal_extractor = Some(extractor);
317        self
318    }
319
320    /// Attach a `SandboxPort` for the Execute stage.
321    pub fn with_sandbox(mut self, sandbox: Arc<dyn SandboxPort>) -> Self {
322        self.sandbox = Some(sandbox);
323        self
324    }
325
326    /// Attach a `GeneStorePersistPort` for the Solidify/Reuse stages.
327    pub fn with_gene_store(mut self, gene_store: Arc<dyn GeneStorePersistPort>) -> Self {
328        self.gene_store = Some(gene_store);
329        self
330    }
331
332    /// Attach a `ValidatePort` for the Validate stage.
333    pub fn with_validate_port(mut self, validator: Arc<dyn ValidatePort>) -> Self {
334        self.validate_port = Some(validator);
335        self
336    }
337
338    /// Attach an `EvaluatePort` for the Evaluate stage.
339    pub fn with_evaluate_port(mut self, evaluator: Arc<dyn EvaluatePort>) -> Self {
340        self.evaluate_port = Some(evaluator);
341        self
342    }
343}
344
345impl EvolutionPipeline for StandardEvolutionPipeline {
346    fn name(&self) -> &str {
347        &self.name
348    }
349
350    fn config(&self) -> &EvolutionPipelineConfig {
351        &self.config
352    }
353
354    fn execute(&self, mut context: PipelineContext) -> Result<PipelineResult, PipelineError> {
355        let mut stage_states = Vec::new();
356
357        // Detect phase
358        if self.config.enable_detect {
359            let mut stage = StageState::new(PipelineStage::Detect.as_str());
360            stage.state = PipelineStageState::Running;
361            stage_states.push(stage);
362
363            let t0 = Instant::now();
364            if let Some(ref extractor) = self.signal_extractor {
365                // Use the injected extractor to populate signals from raw input.
366                let input = context.extractor_input.clone().unwrap_or_default();
367                let extracted = extractor.extract(&input);
368                // Merge: keep any pre-populated signals and append new ones.
369                context.signals.extend(extracted);
370            }
371            // When no extractor is injected, signals already in context are
372            // used as-is (pass-through, backward-compatible behaviour).
373            let elapsed = t0.elapsed();
374            context
375                .stage_timings
376                .insert(PipelineStage::Detect.as_str().to_string(), elapsed);
377            let d_ms = elapsed.as_millis() as u64;
378            let last = stage_states.last_mut().unwrap();
379            last.state = PipelineStageState::Completed;
380            last.duration_ms = Some(d_ms);
381        } else {
382            stage_states.push(StageState {
383                stage_name: PipelineStage::Detect.as_str().to_string(),
384                state: PipelineStageState::Skipped("disabled".to_string()),
385                duration_ms: None,
386            });
387        }
388
389        // Select phase
390        if self.config.enable_select {
391            let mut stage = StageState::new(PipelineStage::Select.as_str());
392            stage.state = PipelineStageState::Running;
393            stage_states.push(stage);
394
395            let t0 = Instant::now();
396            let input = SelectorInput {
397                signals: context
398                    .signals
399                    .iter()
400                    .map(|s| s.description.clone())
401                    .collect(),
402                env: crate::core::EnvFingerprint {
403                    rustc_version: String::new(),
404                    cargo_lock_hash: String::new(),
405                    target_triple: String::new(),
406                    os: String::new(),
407                },
408                spec_id: None,
409                limit: self.config.max_candidates,
410            };
411
412            context.candidates = self.selector.select(&input);
413            let elapsed = t0.elapsed();
414            context
415                .stage_timings
416                .insert(PipelineStage::Select.as_str().to_string(), elapsed);
417            let last = stage_states.last_mut().unwrap();
418            last.state = PipelineStageState::Completed;
419            last.duration_ms = Some(elapsed.as_millis() as u64);
420        } else {
421            stage_states.push(StageState {
422                stage_name: PipelineStage::Select.as_str().to_string(),
423                state: PipelineStageState::Skipped("disabled".to_string()),
424                duration_ms: None,
425            });
426        }
427
428        // Mutate phase - prepare proposals from candidates
429        if self.config.enable_mutate {
430            let mut stage = StageState::new(PipelineStage::Mutate.as_str());
431            stage.state = PipelineStageState::Running;
432            stage_states.push(stage);
433
434            let t0 = Instant::now();
435            context.proposals = context
436                .candidates
437                .iter()
438                .enumerate()
439                .map(|(i, candidate)| MutationProposal {
440                    proposal_id: format!("proposal_{}", i),
441                    signal_ids: vec![],
442                    gene_id: candidate.gene.id.clone(),
443                    description: format!("Mutation for gene {}", candidate.gene.id),
444                    estimated_impact: candidate.score,
445                    risk_level: MutationRiskLevel::Medium,
446                    proposed_changes: serde_json::json!({}),
447                })
448                .collect();
449            let elapsed = t0.elapsed();
450            context
451                .stage_timings
452                .insert(PipelineStage::Mutate.as_str().to_string(), elapsed);
453            let last = stage_states.last_mut().unwrap();
454            last.state = PipelineStageState::Completed;
455            last.duration_ms = Some(elapsed.as_millis() as u64);
456        } else {
457            stage_states.push(StageState {
458                stage_name: PipelineStage::Mutate.as_str().to_string(),
459                state: PipelineStageState::Skipped("disabled".to_string()),
460                duration_ms: None,
461            });
462        }
463
464        // Execute phase
465        if self.config.enable_execute {
466            let mut stage = StageState::new(PipelineStage::Execute.as_str());
467            stage.state = PipelineStageState::Running;
468            stage_states.push(stage);
469
470            let t0 = Instant::now();
471            if let (Some(ref sb), Some(proposal)) = (&self.sandbox, context.proposals.first()) {
472                // Build a minimal PreparedMutation from the first proposal so
473                // the sandbox can apply the change in an isolated workspace.
474                let mutation = build_prepared_mutation(proposal);
475                let result = sb.execute(&mutation);
476                context.execution_result = Some(result.to_json());
477                let last = stage_states.last_mut().unwrap();
478                last.state = if result.success {
479                    PipelineStageState::Completed
480                } else {
481                    PipelineStageState::Failed(result.message.clone())
482                };
483            } else {
484                // No sandbox injected — fall back to stub (backward-compatible).
485                context.execution_result = Some(serde_json::json!({
486                    "success": true,
487                    "stdout": "",
488                    "stderr": "",
489                    "duration_ms": 0,
490                    "message": "Mutation executed successfully (stub)"
491                }));
492                stage_states.last_mut().unwrap().state = PipelineStageState::Completed;
493            }
494            let elapsed = t0.elapsed();
495            context
496                .stage_timings
497                .insert(PipelineStage::Execute.as_str().to_string(), elapsed);
498            stage_states.last_mut().unwrap().duration_ms = Some(elapsed.as_millis() as u64);
499        } else {
500            stage_states.push(StageState {
501                stage_name: PipelineStage::Execute.as_str().to_string(),
502                state: PipelineStageState::Skipped("disabled".to_string()),
503                duration_ms: None,
504            });
505        }
506
507        // Validate phase
508        if self.config.enable_validate {
509            let mut stage = StageState::new(PipelineStage::Validate.as_str());
510            stage.state = PipelineStageState::Running;
511            stage_states.push(stage);
512
513            let t0 = Instant::now();
514            if let Some(proposal) = context.proposals.first() {
515                let vresult = if let Some(ref vp) = self.validate_port {
516                    // Build input from execution result stored in context.
517                    let exec = context.execution_result.as_ref();
518                    let exec_success = exec
519                        .and_then(|v| v.get("success"))
520                        .and_then(|v| v.as_bool())
521                        .unwrap_or(false);
522                    let stdout = exec
523                        .and_then(|v| v.get("stdout"))
524                        .and_then(|v| v.as_str())
525                        .unwrap_or("")
526                        .to_string();
527                    let stderr = exec
528                        .and_then(|v| v.get("stderr"))
529                        .and_then(|v| v.as_str())
530                        .unwrap_or("")
531                        .to_string();
532                    let input = ValidateInput {
533                        proposal_id: proposal.proposal_id.clone(),
534                        execution_success: exec_success,
535                        stdout,
536                        stderr,
537                    };
538                    vp.validate(&input)
539                } else {
540                    // Backward-compatible stub when no validator is injected.
541                    ValidationResult {
542                        proposal_id: proposal.proposal_id.clone(),
543                        passed: true,
544                        score: 0.8,
545                        issues: vec![],
546                        simulation_results: None,
547                    }
548                };
549                context.validation_result = Some(vresult);
550            }
551            let elapsed = t0.elapsed();
552            context
553                .stage_timings
554                .insert(PipelineStage::Validate.as_str().to_string(), elapsed);
555            let last = stage_states.last_mut().unwrap();
556            // Mark stage Failed when validation did not pass so callers can detect it.
557            last.state = match &context.validation_result {
558                Some(r) if !r.passed => PipelineStageState::Failed("validation failed".to_string()),
559                _ => PipelineStageState::Completed,
560            };
561            last.duration_ms = Some(elapsed.as_millis() as u64);
562        } else {
563            stage_states.push(StageState {
564                stage_name: PipelineStage::Validate.as_str().to_string(),
565                state: PipelineStageState::Skipped("disabled".to_string()),
566                duration_ms: None,
567            });
568        }
569
570        // Evaluate phase
571        if self.config.enable_evaluate {
572            let mut stage = StageState::new(PipelineStage::Evaluate.as_str());
573            stage.state = PipelineStageState::Running;
574            stage_states.push(stage);
575
576            let t0 = Instant::now();
577            context.evaluation_result = if let Some(ref ep) = self.evaluate_port {
578                if let Some(proposal) = context.proposals.first() {
579                    let input = EvaluateInput {
580                        proposal_id: proposal.proposal_id.clone(),
581                        intent: proposal.description.clone(),
582                        original: String::new(),
583                        proposed: String::new(),
584                        signals: context
585                            .signals
586                            .iter()
587                            .map(|s| s.description.clone())
588                            .collect(),
589                    };
590                    Some(ep.evaluate(&input))
591                } else {
592                    None
593                }
594            } else {
595                // Backward-compatible stub when no evaluator is injected.
596                Some(EvaluationResult {
597                    score: 0.8,
598                    improvements: vec!["Mutation applied successfully".to_string()],
599                    regressions: vec![],
600                    recommendation: EvaluationRecommendation::Accept,
601                })
602            };
603            let elapsed = t0.elapsed();
604            context
605                .stage_timings
606                .insert(PipelineStage::Evaluate.as_str().to_string(), elapsed);
607            let last = stage_states.last_mut().unwrap();
608            last.state = PipelineStageState::Completed;
609            last.duration_ms = Some(elapsed.as_millis() as u64);
610        } else {
611            stage_states.push(StageState {
612                stage_name: PipelineStage::Evaluate.as_str().to_string(),
613                state: PipelineStageState::Skipped("disabled".to_string()),
614                duration_ms: None,
615            });
616        }
617
618        // Solidify phase - persist promoted genes via the GeneStorePersistPort
619        if self.config.enable_solidify {
620            let mut stage = StageState::new(PipelineStage::Solidify.as_str());
621            stage.state = PipelineStageState::Running;
622            stage_states.push(stage);
623
624            let t0 = Instant::now();
625            let mut solidified: Vec<String> = Vec::new();
626            for candidate in &context.candidates {
627                let gene = &candidate.gene;
628                // Persist via injected port when available.
629                if let Some(ref store) = self.gene_store {
630                    store.persist_gene(&gene.id, &gene.signals, &gene.strategy, &gene.validation);
631                }
632                solidified.push(gene.id.clone());
633            }
634            context.solidified_genes = solidified;
635            let elapsed = t0.elapsed();
636            context
637                .stage_timings
638                .insert(PipelineStage::Solidify.as_str().to_string(), elapsed);
639            let last = stage_states.last_mut().unwrap();
640            last.state = PipelineStageState::Completed;
641            last.duration_ms = Some(elapsed.as_millis() as u64);
642        } else {
643            stage_states.push(StageState {
644                stage_name: PipelineStage::Solidify.as_str().to_string(),
645                state: PipelineStageState::Skipped("disabled".to_string()),
646                duration_ms: None,
647            });
648        }
649
650        // Reuse phase - record capsule reuse via the GeneStorePersistPort
651        if self.config.enable_reuse {
652            let mut stage = StageState::new(PipelineStage::Reuse.as_str());
653            stage.state = PipelineStageState::Running;
654            stage_states.push(stage);
655
656            let t0 = Instant::now();
657            let mut reused: Vec<String> = Vec::new();
658            for candidate in &context.candidates {
659                let cap_ids: Vec<String> =
660                    candidate.capsules.iter().map(|c| c.id.clone()).collect();
661                if let Some(ref store) = self.gene_store {
662                    store.mark_reused(&candidate.gene.id, &cap_ids);
663                }
664                reused.extend(cap_ids);
665            }
666            context.reused_capsules = reused;
667            let elapsed = t0.elapsed();
668            context
669                .stage_timings
670                .insert(PipelineStage::Reuse.as_str().to_string(), elapsed);
671            let last = stage_states.last_mut().unwrap();
672            last.state = PipelineStageState::Completed;
673            last.duration_ms = Some(elapsed.as_millis() as u64);
674        } else {
675            stage_states.push(StageState {
676                stage_name: PipelineStage::Reuse.as_str().to_string(),
677                state: PipelineStageState::Skipped("disabled".to_string()),
678                duration_ms: None,
679            });
680        }
681
682        // Propagate validation failure to the overall pipeline result.
683        let validation_passed = context
684            .validation_result
685            .as_ref()
686            .map(|r| r.passed)
687            .unwrap_or(true);
688
689        Ok(PipelineResult {
690            success: validation_passed,
691            stage_states,
692            error: if validation_passed {
693                None
694            } else {
695                Some("Validation stage did not pass".to_string())
696            },
697        })
698    }
699
700    fn execute_stage(
701        &self,
702        stage: PipelineStage,
703        context: &mut PipelineContext,
704    ) -> Result<PipelineStageState, PipelineError> {
705        match stage {
706            PipelineStage::Detect => {
707                // Signals already in context
708                Ok(PipelineStageState::Completed)
709            }
710            PipelineStage::Select => {
711                let input = SelectorInput {
712                    signals: context
713                        .signals
714                        .iter()
715                        .map(|s| s.description.clone())
716                        .collect(),
717                    env: crate::core::EnvFingerprint {
718                        rustc_version: String::new(),
719                        cargo_lock_hash: String::new(),
720                        target_triple: String::new(),
721                        os: String::new(),
722                    },
723                    spec_id: None,
724                    limit: self.config.max_candidates,
725                };
726                context.candidates = self.selector.select(&input);
727                Ok(PipelineStageState::Completed)
728            }
729            PipelineStage::Mutate => {
730                context.proposals = context
731                    .candidates
732                    .iter()
733                    .enumerate()
734                    .map(|(i, candidate)| MutationProposal {
735                        proposal_id: format!("proposal_{}", i),
736                        signal_ids: vec![],
737                        gene_id: candidate.gene.id.clone(),
738                        description: format!("Mutation for gene {}", candidate.gene.id),
739                        estimated_impact: candidate.score,
740                        risk_level: MutationRiskLevel::Medium,
741                        proposed_changes: serde_json::json!({}),
742                    })
743                    .collect();
744                Ok(PipelineStageState::Completed)
745            }
746            PipelineStage::Execute => {
747                context.execution_result = Some(serde_json::json!({
748                    "status": "success"
749                }));
750                Ok(PipelineStageState::Completed)
751            }
752            PipelineStage::Validate => {
753                if let Some(proposal) = context.proposals.first() {
754                    let validation_result = if let Some(ref validator) = self.validate_port {
755                        let exec = context.execution_result.as_ref();
756                        let input = ValidateInput {
757                            proposal_id: proposal.proposal_id.clone(),
758                            execution_success: exec
759                                .and_then(|value| value.get("success"))
760                                .and_then(|value| value.as_bool())
761                                .unwrap_or(false),
762                            stdout: exec
763                                .and_then(|value| value.get("stdout"))
764                                .and_then(|value| value.as_str())
765                                .unwrap_or("")
766                                .to_string(),
767                            stderr: exec
768                                .and_then(|value| value.get("stderr"))
769                                .and_then(|value| value.as_str())
770                                .unwrap_or("")
771                                .to_string(),
772                        };
773                        validator.validate(&input)
774                    } else {
775                        ValidationResult {
776                            proposal_id: proposal.proposal_id.clone(),
777                            passed: true,
778                            score: 0.8,
779                            issues: vec![],
780                            simulation_results: None,
781                        }
782                    };
783                    context.validation_result = Some(validation_result);
784                }
785                Ok(match context.validation_result.as_ref() {
786                    Some(result) if !result.passed => {
787                        PipelineStageState::Failed("validation failed".to_string())
788                    }
789                    _ => PipelineStageState::Completed,
790                })
791            }
792            PipelineStage::Evaluate => {
793                context.evaluation_result = if let Some(ref evaluator) = self.evaluate_port {
794                    context.proposals.first().map(|proposal| {
795                        evaluator.evaluate(&EvaluateInput {
796                            proposal_id: proposal.proposal_id.clone(),
797                            intent: proposal.description.clone(),
798                            original: String::new(),
799                            proposed: String::new(),
800                            signals: context
801                                .signals
802                                .iter()
803                                .map(|signal| signal.description.clone())
804                                .collect(),
805                        })
806                    })
807                } else {
808                    Some(EvaluationResult {
809                        score: 0.8,
810                        improvements: vec!["Mutation applied successfully".to_string()],
811                        regressions: vec![],
812                        recommendation: EvaluationRecommendation::Accept,
813                    })
814                };
815                Ok(PipelineStageState::Completed)
816            }
817            PipelineStage::Solidify => {
818                for candidate in &context.candidates {
819                    let gene = &candidate.gene;
820                    if let Some(ref store) = self.gene_store {
821                        store.persist_gene(
822                            &gene.id,
823                            &gene.signals,
824                            &gene.strategy,
825                            &gene.validation,
826                        );
827                    }
828                }
829                context.solidified_genes = context
830                    .candidates
831                    .iter()
832                    .map(|c| c.gene.id.clone())
833                    .collect();
834                Ok(PipelineStageState::Completed)
835            }
836            PipelineStage::Reuse => {
837                for candidate in &context.candidates {
838                    let cap_ids: Vec<String> =
839                        candidate.capsules.iter().map(|c| c.id.clone()).collect();
840                    if let Some(ref store) = self.gene_store {
841                        store.mark_reused(&candidate.gene.id, &cap_ids);
842                    }
843                    context.reused_capsules.extend(cap_ids);
844                }
845                Ok(PipelineStageState::Completed)
846            }
847        }
848    }
849}
850
851/// Build a minimal `PreparedMutation` from a `MutationProposal`.
852///
853/// Used by the Execute stage when a `SandboxPort` is injected. The resulting
854/// mutation carries the proposal identifier and an empty unified-diff payload;
855/// real mutation generation (LLM or rule-based) will replace this in a later
856/// phase of the evolution pipeline.
857fn build_prepared_mutation(proposal: &MutationProposal) -> PreparedMutation {
858    use crate::core::{
859        ArtifactEncoding, MutationArtifact, MutationIntent, MutationTarget, RiskLevel,
860    };
861
862    PreparedMutation {
863        intent: MutationIntent {
864            id: proposal.proposal_id.clone(),
865            intent: proposal.description.clone(),
866            target: MutationTarget::WorkspaceRoot,
867            expected_effect: format!("Apply mutation for gene {}", proposal.gene_id),
868            risk: match proposal.risk_level {
869                MutationRiskLevel::Low => RiskLevel::Low,
870                MutationRiskLevel::Medium => RiskLevel::Medium,
871                MutationRiskLevel::High | MutationRiskLevel::Critical => RiskLevel::High,
872            },
873            signals: proposal.signal_ids.clone(),
874            spec_id: None,
875        },
876        artifact: MutationArtifact {
877            encoding: ArtifactEncoding::UnifiedDiff,
878            payload: String::new(),
879            base_revision: None,
880            content_hash: String::new(),
881        },
882    }
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use std::sync::atomic::{AtomicUsize, Ordering};
889
890    #[test]
891    fn test_pipeline_config_default() {
892        let config = EvolutionPipelineConfig::default();
893        assert!(config.enable_detect);
894        assert!(config.enable_select);
895        assert!(config.enable_mutate);
896    }
897
898    #[test]
899    fn test_pipeline_stage_states() {
900        let config = EvolutionPipelineConfig {
901            enable_detect: false,
902            enable_select: true,
903            enable_mutate: false,
904            ..Default::default()
905        };
906
907        // Just test that config works
908        assert!(!config.enable_detect);
909        assert!(config.enable_select);
910    }
911
912    // ─── GeneStorePersistPort integration test ─────────────────────────────
913
914    /// A minimal in-memory mock that records which genes/capsules were persisted.
915    struct MockGeneStore {
916        genes: std::sync::Mutex<Vec<String>>,
917        reused: std::sync::Mutex<Vec<String>>,
918    }
919
920    impl MockGeneStore {
921        fn new() -> Self {
922            Self {
923                genes: std::sync::Mutex::new(Vec::new()),
924                reused: std::sync::Mutex::new(Vec::new()),
925            }
926        }
927    }
928
929    impl GeneStorePersistPort for MockGeneStore {
930        fn persist_gene(
931            &self,
932            gene_id: &str,
933            _signals: &[String],
934            _strategy: &[String],
935            _validation: &[String],
936        ) -> bool {
937            self.genes.lock().unwrap().push(gene_id.to_string());
938            true
939        }
940
941        fn mark_reused(&self, gene_id: &str, _capsule_ids: &[String]) -> bool {
942            self.reused.lock().unwrap().push(gene_id.to_string());
943            true
944        }
945    }
946
947    /// A minimal `Selector` that always returns one hard-coded candidate.
948    struct SingleCandidateSelector;
949
950    impl Selector for SingleCandidateSelector {
951        fn select(&self, _input: &SelectorInput) -> Vec<GeneCandidate> {
952            use crate::core;
953            vec![GeneCandidate {
954                gene: core::Gene {
955                    id: "gene-abc-001".to_string(),
956                    signals: vec!["test-signal".to_string()],
957                    strategy: vec!["apply-fix".to_string()],
958                    validation: vec!["cargo test".to_string()],
959                    state: core::AssetState::default(),
960                    task_class_id: None,
961                },
962                capsules: vec![],
963                score: 0.9,
964            }]
965        }
966    }
967
968    struct RecordingValidatePort {
969        passed: bool,
970        calls: std::sync::Mutex<Vec<ValidateInput>>,
971    }
972
973    impl RecordingValidatePort {
974        fn new(passed: bool) -> Self {
975            Self {
976                passed,
977                calls: std::sync::Mutex::new(Vec::new()),
978            }
979        }
980    }
981
982    impl ValidatePort for RecordingValidatePort {
983        fn validate(&self, input: &ValidateInput) -> ValidationResult {
984            self.calls.lock().unwrap().push(input.clone());
985            ValidationResult {
986                proposal_id: input.proposal_id.clone(),
987                passed: self.passed,
988                score: if self.passed { 1.0 } else { 0.2 },
989                issues: vec![],
990                simulation_results: None,
991            }
992        }
993    }
994
995    struct RecordingEvaluatePort {
996        calls: std::sync::Mutex<Vec<EvaluateInput>>,
997    }
998
999    impl RecordingEvaluatePort {
1000        fn new() -> Self {
1001            Self {
1002                calls: std::sync::Mutex::new(Vec::new()),
1003            }
1004        }
1005    }
1006
1007    struct CountingValidatePort {
1008        call_count: Arc<AtomicUsize>,
1009        passed: bool,
1010    }
1011
1012    impl CountingValidatePort {
1013        fn new(call_count: Arc<AtomicUsize>, passed: bool) -> Self {
1014            Self { call_count, passed }
1015        }
1016    }
1017
1018    impl ValidatePort for CountingValidatePort {
1019        fn validate(&self, input: &ValidateInput) -> ValidationResult {
1020            self.call_count.fetch_add(1, Ordering::SeqCst);
1021            ValidationResult {
1022                proposal_id: input.proposal_id.clone(),
1023                passed: self.passed,
1024                score: if self.passed { 0.95 } else { 0.15 },
1025                issues: vec![],
1026                simulation_results: None,
1027            }
1028        }
1029    }
1030
1031    struct CountingEvaluatePort {
1032        call_count: Arc<AtomicUsize>,
1033    }
1034
1035    impl CountingEvaluatePort {
1036        fn new(call_count: Arc<AtomicUsize>) -> Self {
1037            Self { call_count }
1038        }
1039    }
1040
1041    impl EvaluatePort for CountingEvaluatePort {
1042        fn evaluate(&self, _input: &EvaluateInput) -> EvaluationResult {
1043            self.call_count.fetch_add(1, Ordering::SeqCst);
1044            EvaluationResult {
1045                score: 0.91,
1046                improvements: vec!["evaluate port called".to_string()],
1047                regressions: vec![],
1048                recommendation: EvaluationRecommendation::Accept,
1049            }
1050        }
1051    }
1052
1053    impl EvaluatePort for RecordingEvaluatePort {
1054        fn evaluate(&self, input: &EvaluateInput) -> EvaluationResult {
1055            self.calls.lock().unwrap().push(input.clone());
1056            EvaluationResult {
1057                score: 0.33,
1058                improvements: vec!["used injected evaluator".to_string()],
1059                regressions: vec!["none".to_string()],
1060                recommendation: EvaluationRecommendation::NeedsRevision,
1061            }
1062        }
1063    }
1064
1065    #[test]
1066    fn test_solidify_reuse_calls_gene_store() {
1067        let store = Arc::new(MockGeneStore::new());
1068        let config = EvolutionPipelineConfig {
1069            enable_detect: false,
1070            enable_select: true,
1071            enable_mutate: false,
1072            enable_execute: false,
1073            enable_validate: false,
1074            enable_evaluate: false,
1075            enable_solidify: true,
1076            enable_reuse: true,
1077            ..Default::default()
1078        };
1079
1080        let pipeline = StandardEvolutionPipeline::new(config, Arc::new(SingleCandidateSelector))
1081            .with_gene_store(store.clone());
1082
1083        let ctx = PipelineContext::default();
1084        let result = pipeline.execute(ctx).expect("pipeline should succeed");
1085        assert!(result.success);
1086
1087        // Verify Solidify stage persisted the gene
1088        let persisted_genes = store.genes.lock().unwrap();
1089        assert!(
1090            persisted_genes.contains(&"gene-abc-001".to_string()),
1091            "Solidify stage should have persisted gene-abc-001, got: {:?}",
1092            persisted_genes
1093        );
1094    }
1095
1096    #[test]
1097    fn test_execute_stage_validate_uses_injected_port_and_fallback() {
1098        let validator = Arc::new(RecordingValidatePort::new(false));
1099        let pipeline = StandardEvolutionPipeline::new(
1100            EvolutionPipelineConfig::default(),
1101            Arc::new(SingleCandidateSelector),
1102        )
1103        .with_validate_port(validator.clone());
1104
1105        let mut context = PipelineContext::default();
1106        context.proposals.push(MutationProposal {
1107            proposal_id: "proposal-1".to_string(),
1108            signal_ids: vec![],
1109            gene_id: "gene-1".to_string(),
1110            description: "validate proposal".to_string(),
1111            estimated_impact: 0.5,
1112            risk_level: MutationRiskLevel::Medium,
1113            proposed_changes: serde_json::json!({}),
1114        });
1115        context.execution_result = Some(serde_json::json!({
1116            "success": true,
1117            "stdout": "validator stdout",
1118            "stderr": ""
1119        }));
1120
1121        let state = pipeline
1122            .execute_stage(PipelineStage::Validate, &mut context)
1123            .expect("validate stage should succeed");
1124
1125        assert_eq!(
1126            state,
1127            PipelineStageState::Failed("validation failed".to_string())
1128        );
1129        let calls = validator.calls.lock().unwrap();
1130        assert_eq!(calls.len(), 1);
1131        assert_eq!(calls[0].proposal_id, "proposal-1");
1132        assert!(calls[0].execution_success);
1133        assert_eq!(calls[0].stdout, "validator stdout");
1134        drop(calls);
1135        assert!(!context.validation_result.as_ref().unwrap().passed);
1136
1137        let fallback_pipeline = StandardEvolutionPipeline::new(
1138            EvolutionPipelineConfig::default(),
1139            Arc::new(SingleCandidateSelector),
1140        );
1141        let mut fallback_context = PipelineContext::default();
1142        fallback_context.proposals.push(MutationProposal {
1143            proposal_id: "proposal-2".to_string(),
1144            signal_ids: vec![],
1145            gene_id: "gene-2".to_string(),
1146            description: "fallback validate".to_string(),
1147            estimated_impact: 0.4,
1148            risk_level: MutationRiskLevel::Low,
1149            proposed_changes: serde_json::json!({}),
1150        });
1151
1152        let fallback_state = fallback_pipeline
1153            .execute_stage(PipelineStage::Validate, &mut fallback_context)
1154            .expect("fallback validate stage should succeed");
1155
1156        assert_eq!(fallback_state, PipelineStageState::Completed);
1157        assert!(fallback_context.validation_result.as_ref().unwrap().passed);
1158        assert_eq!(
1159            fallback_context.validation_result.as_ref().unwrap().score,
1160            0.8
1161        );
1162    }
1163
1164    #[test]
1165    fn test_execute_stage_evaluate_uses_injected_port_and_fallback() {
1166        let evaluator = Arc::new(RecordingEvaluatePort::new());
1167        let pipeline = StandardEvolutionPipeline::new(
1168            EvolutionPipelineConfig::default(),
1169            Arc::new(SingleCandidateSelector),
1170        )
1171        .with_evaluate_port(evaluator.clone());
1172
1173        let mut context = PipelineContext::default();
1174        context.signals.push(EvolutionSignal {
1175            signal_id: "signal-1".to_string(),
1176            signal_type: crate::evolver::SignalType::ErrorPattern {
1177                error_type: "panic".to_string(),
1178                frequency: 1,
1179            },
1180            source_task_id: "task-1".to_string(),
1181            confidence: 0.9,
1182            description: "improve evaluator path".to_string(),
1183            metadata: serde_json::json!({}),
1184        });
1185        context.proposals.push(MutationProposal {
1186            proposal_id: "proposal-3".to_string(),
1187            signal_ids: vec!["signal-1".to_string()],
1188            gene_id: "gene-3".to_string(),
1189            description: "evaluate proposal".to_string(),
1190            estimated_impact: 0.7,
1191            risk_level: MutationRiskLevel::Medium,
1192            proposed_changes: serde_json::json!({}),
1193        });
1194
1195        let state = pipeline
1196            .execute_stage(PipelineStage::Evaluate, &mut context)
1197            .expect("evaluate stage should succeed");
1198
1199        assert_eq!(state, PipelineStageState::Completed);
1200        let calls = evaluator.calls.lock().unwrap();
1201        assert_eq!(calls.len(), 1);
1202        assert_eq!(calls[0].proposal_id, "proposal-3");
1203        assert_eq!(calls[0].intent, "evaluate proposal");
1204        assert_eq!(calls[0].signals, vec!["improve evaluator path".to_string()]);
1205        drop(calls);
1206        assert_eq!(
1207            context.evaluation_result.as_ref().unwrap().recommendation,
1208            EvaluationRecommendation::NeedsRevision
1209        );
1210        assert_eq!(context.evaluation_result.as_ref().unwrap().score, 0.33);
1211
1212        let fallback_pipeline = StandardEvolutionPipeline::new(
1213            EvolutionPipelineConfig::default(),
1214            Arc::new(SingleCandidateSelector),
1215        );
1216        let mut fallback_context = PipelineContext::default();
1217
1218        let fallback_state = fallback_pipeline
1219            .execute_stage(PipelineStage::Evaluate, &mut fallback_context)
1220            .expect("fallback evaluate stage should succeed");
1221
1222        assert_eq!(fallback_state, PipelineStageState::Completed);
1223        assert_eq!(
1224            fallback_context
1225                .evaluation_result
1226                .as_ref()
1227                .unwrap()
1228                .recommendation,
1229            EvaluationRecommendation::Accept
1230        );
1231        assert_eq!(
1232            fallback_context
1233                .evaluation_result
1234                .as_ref()
1235                .unwrap()
1236                .improvements,
1237            vec!["Mutation applied successfully".to_string()]
1238        );
1239    }
1240
1241    #[test]
1242    fn test_execute_invokes_injected_validate_and_evaluate_ports() {
1243        let validate_calls = Arc::new(AtomicUsize::new(0));
1244        let evaluate_calls = Arc::new(AtomicUsize::new(0));
1245        let config = EvolutionPipelineConfig {
1246            enable_detect: false,
1247            enable_select: true,
1248            enable_mutate: true,
1249            enable_execute: true,
1250            enable_validate: true,
1251            enable_evaluate: true,
1252            enable_solidify: false,
1253            enable_reuse: false,
1254            ..Default::default()
1255        };
1256
1257        let pipeline = StandardEvolutionPipeline::new(config, Arc::new(SingleCandidateSelector))
1258            .with_validate_port(Arc::new(CountingValidatePort::new(
1259                validate_calls.clone(),
1260                true,
1261            )))
1262            .with_evaluate_port(Arc::new(CountingEvaluatePort::new(evaluate_calls.clone())));
1263
1264        let result = pipeline
1265            .execute(PipelineContext::default())
1266            .expect("pipeline should execute");
1267
1268        assert!(result.success);
1269        assert_eq!(validate_calls.load(Ordering::SeqCst), 1);
1270        assert_eq!(evaluate_calls.load(Ordering::SeqCst), 1);
1271        assert!(result.error.is_none());
1272        assert!(result.stage_states.iter().any(|stage| {
1273            stage.stage_name == PipelineStage::Validate.as_str()
1274                && stage.state == PipelineStageState::Completed
1275        }));
1276        assert!(result.stage_states.iter().any(|stage| {
1277            stage.stage_name == PipelineStage::Evaluate.as_str()
1278                && stage.state == PipelineStageState::Completed
1279        }));
1280    }
1281
1282    #[test]
1283    fn test_execute_propagates_validate_port_failure_to_pipeline_result() {
1284        let validate_calls = Arc::new(AtomicUsize::new(0));
1285        let evaluate_calls = Arc::new(AtomicUsize::new(0));
1286        let config = EvolutionPipelineConfig {
1287            enable_detect: false,
1288            enable_select: true,
1289            enable_mutate: true,
1290            enable_execute: true,
1291            enable_validate: true,
1292            enable_evaluate: true,
1293            enable_solidify: false,
1294            enable_reuse: false,
1295            ..Default::default()
1296        };
1297
1298        let pipeline = StandardEvolutionPipeline::new(config, Arc::new(SingleCandidateSelector))
1299            .with_validate_port(Arc::new(CountingValidatePort::new(
1300                validate_calls.clone(),
1301                false,
1302            )))
1303            .with_evaluate_port(Arc::new(CountingEvaluatePort::new(evaluate_calls.clone())));
1304
1305        let result = pipeline
1306            .execute(PipelineContext::default())
1307            .expect("pipeline should execute");
1308
1309        assert!(!result.success);
1310        assert_eq!(validate_calls.load(Ordering::SeqCst), 1);
1311        assert_eq!(evaluate_calls.load(Ordering::SeqCst), 1);
1312        assert_eq!(
1313            result.error.as_deref(),
1314            Some("Validation stage did not pass")
1315        );
1316        assert!(result.stage_states.iter().any(|stage| {
1317            stage.stage_name == PipelineStage::Validate.as_str()
1318                && stage.state == PipelineStageState::Failed("validation failed".to_string())
1319        }));
1320    }
1321}