1use 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#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct EvolutionPipelineConfig {
29 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 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 pub max_candidates: usize,
51
52 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#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
83pub enum PipelineStageState {
84 Pending,
86 Running,
88 Completed,
90 Failed(String),
92 Skipped(String),
94}
95
96pub struct PipelineContext {
98 pub task_input: serde_json::Value,
100 pub extractor_input: Option<SignalExtractorInput>,
104 pub signals: Vec<EvolutionSignal>,
106 pub candidates: Vec<GeneCandidate>,
108 pub proposals: Vec<MutationProposal>,
110 pub execution_result: Option<serde_json::Value>,
112 pub validation_result: Option<ValidationResult>,
114 pub evaluation_result: Option<EvaluationResult>,
116 pub solidified_genes: Vec<String>,
118 pub reused_capsules: Vec<String>,
120 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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct PipelineResult {
162 pub success: bool,
164 pub stage_states: Vec<StageState>,
166 pub error: Option<String>,
168}
169
170#[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#[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
219pub trait EvolutionPipeline: Send + Sync {
221 fn name(&self) -> &str;
223
224 fn config(&self) -> &EvolutionPipelineConfig;
226
227 fn execute(&self, context: PipelineContext) -> Result<PipelineResult, PipelineError>;
229
230 #[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#[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
280pub struct StandardEvolutionPipeline {
282 name: String,
283 config: EvolutionPipelineConfig,
284 selector: Arc<dyn Selector>,
285 signal_extractor: Option<Arc<dyn SignalExtractorPort>>,
287 sandbox: Option<Arc<dyn SandboxPort>>,
289 gene_store: Option<Arc<dyn GeneStorePersistPort>>,
291 validate_port: Option<Arc<dyn ValidatePort>>,
293 evaluate_port: Option<Arc<dyn EvaluatePort>>,
295}
296
297impl StandardEvolutionPipeline {
298 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 pub fn with_signal_extractor(mut self, extractor: Arc<dyn SignalExtractorPort>) -> Self {
316 self.signal_extractor = Some(extractor);
317 self
318 }
319
320 pub fn with_sandbox(mut self, sandbox: Arc<dyn SandboxPort>) -> Self {
322 self.sandbox = Some(sandbox);
323 self
324 }
325
326 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 pub fn with_validate_port(mut self, validator: Arc<dyn ValidatePort>) -> Self {
334 self.validate_port = Some(validator);
335 self
336 }
337
338 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 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 let input = context.extractor_input.clone().unwrap_or_default();
367 let extracted = extractor.extract(&input);
368 context.signals.extend(extracted);
370 }
371 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
851fn 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 assert!(!config.enable_detect);
909 assert!(config.enable_select);
910 }
911
912 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 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 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}