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