1use std::path::PathBuf;
26
27use oris_evolution::{
28 EvaluateInput, EvaluatePort, EvaluationRecommendation, EvaluationResult, EvolutionSignal,
29 GeneStorePersistPort, IssueSeverity, PreparedMutation, SandboxExecutionResult, SandboxPort,
30 SignalExtractorInput, SignalExtractorPort, SignalType, ValidateInput, ValidatePort,
31 ValidationIssue, ValidationResult,
32};
33use oris_sandbox::{LocalProcessSandbox, Sandbox, SandboxError, SandboxPolicy};
34
35use crate::signal_extractor::RuntimeSignalExtractor;
36pub struct RuntimeSignalExtractorAdapter {
45 inner: RuntimeSignalExtractor,
46}
47
48impl RuntimeSignalExtractorAdapter {
49 pub fn new() -> Self {
51 Self {
52 inner: RuntimeSignalExtractor::new(),
53 }
54 }
55}
56
57impl Default for RuntimeSignalExtractorAdapter {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl SignalExtractorPort for RuntimeSignalExtractorAdapter {
64 fn extract(&self, input: &SignalExtractorInput) -> Vec<EvolutionSignal> {
65 let runtime_signals = self.inner.extract_all(
66 input.compiler_output.as_deref(),
67 input.stack_trace.as_deref(),
68 input.logs.as_deref(),
69 );
70
71 runtime_signals
72 .into_iter()
73 .map(|rs| {
74 use crate::signal_extractor::RuntimeSignalType;
75 let signal_type = match rs.signal_type {
76 RuntimeSignalType::PerformanceIssue => SignalType::Performance {
77 metric: "runtime".to_string(),
78 improvement_potential: rs.confidence,
79 },
80 RuntimeSignalType::ResourceExhaustion => SignalType::ResourceOptimization {
81 resource_type: "system".to_string(),
82 current_usage: rs.confidence,
83 },
84 RuntimeSignalType::CompilerDiagnostic
85 | RuntimeSignalType::RuntimePanic
86 | RuntimeSignalType::Timeout
87 | RuntimeSignalType::TestFailure
88 | RuntimeSignalType::ConfigError
89 | RuntimeSignalType::SecurityIssue
90 | RuntimeSignalType::GenericError => SignalType::ErrorPattern {
91 error_type: format!("{:?}", rs.signal_type),
92 frequency: 1,
93 },
94 };
95
96 EvolutionSignal {
97 signal_id: rs.signal_id,
98 signal_type,
99 source_task_id: String::new(),
100 confidence: rs.confidence,
101 description: rs.content,
102 metadata: rs.metadata,
103 }
104 })
105 .collect()
106 }
107}
108
109pub struct LocalSandboxAdapter {
119 inner: LocalProcessSandbox,
120 policy: SandboxPolicy,
121}
122
123impl LocalSandboxAdapter {
124 pub fn new<S, P, Q>(run_id: S, workspace_root: P, temp_root: Q) -> Self
130 where
131 S: Into<String>,
132 P: Into<PathBuf>,
133 Q: Into<PathBuf>,
134 {
135 Self {
136 inner: LocalProcessSandbox::new(run_id, workspace_root, temp_root),
137 policy: SandboxPolicy::default(),
138 }
139 }
140
141 pub fn with_policy(mut self, policy: SandboxPolicy) -> Self {
143 self.policy = policy;
144 self
145 }
146}
147
148impl SandboxPort for LocalSandboxAdapter {
149 fn execute(&self, mutation: &PreparedMutation) -> SandboxExecutionResult {
150 let policy = self.policy.clone();
152
153 let mutation_owned = mutation.clone();
156 let inner = &self.inner;
157
158 let run_result: Result<_, SandboxError> = {
162 match tokio::runtime::Handle::try_current() {
163 Ok(handle) => {
164 std::thread::scope(|s| {
167 s.spawn(|| handle.block_on(inner.apply(&mutation_owned, &policy)))
168 .join()
169 .unwrap_or_else(|_| {
170 Err(SandboxError::Io("sandbox thread panicked".into()))
171 })
172 })
173 }
174 Err(_) => {
175 tokio::runtime::Runtime::new()
177 .map_err(|e| SandboxError::Io(e.to_string()))
178 .and_then(|rt| rt.block_on(inner.apply(&mutation_owned, &policy)))
179 }
180 }
181 };
182
183 match run_result {
184 Ok(receipt) => {
185 let changed: Vec<String> = receipt
186 .changed_files
187 .iter()
188 .map(|p| p.display().to_string())
189 .collect();
190 SandboxExecutionResult {
191 success: receipt.applied,
192 stdout: changed.join("\n"),
193 stderr: String::new(),
194 duration_ms: 0,
195 message: if receipt.applied {
196 "Sandbox mutation applied successfully".to_string()
197 } else {
198 "Sandbox applied but patch was not marked as applied".to_string()
199 },
200 }
201 }
202 Err(e) => SandboxExecutionResult::failure(
203 format!("{e}"),
204 format!("Sandbox execution failed: {e}"),
205 0,
206 ),
207 }
208 }
209}
210
211pub struct SqliteGeneStorePersistAdapter {
225 store: oris_genestore::SqliteGeneStore,
226}
227
228impl SqliteGeneStorePersistAdapter {
229 pub fn open(path: &str) -> anyhow::Result<Self> {
231 Ok(Self {
232 store: oris_genestore::SqliteGeneStore::open(path)?,
233 })
234 }
235}
236
237impl GeneStorePersistPort for SqliteGeneStorePersistAdapter {
238 fn persist_gene(
239 &self,
240 gene_id: &str,
241 signals: &[String],
242 strategy: &[String],
243 validation: &[String],
244 ) -> bool {
245 use chrono::Utc;
246 use oris_genestore::{Gene, GeneStore};
247 use uuid::Uuid;
248
249 let id = Uuid::parse_str(gene_id).unwrap_or_else(|_| Uuid::new_v4());
250 let gene = Gene {
251 id,
252 name: format!("gene-{}", &gene_id[..gene_id.len().min(8)]),
253 description: signals.first().cloned().unwrap_or_default(),
254 tags: signals.to_vec(),
255 template: strategy.join("\n"),
256 preconditions: vec![],
257 validation_steps: validation.to_vec(),
258 confidence: 0.70,
259 use_count: 0,
260 success_count: 0,
261 quality_score: 0.60,
262 created_at: Utc::now(),
263 last_used_at: None,
264 last_boosted_at: None,
265 };
266
267 let store = &self.store;
268 let result = match tokio::runtime::Handle::try_current() {
269 Ok(handle) => std::thread::scope(|s| {
270 s.spawn(|| handle.block_on(store.upsert_gene(&gene)))
271 .join()
272 .unwrap_or_else(|_| Err(anyhow::anyhow!("thread panicked")))
273 }),
274 Err(_) => tokio::runtime::Runtime::new()
275 .map_err(|e| anyhow::anyhow!(e))
276 .and_then(|rt| rt.block_on(store.upsert_gene(&gene))),
277 };
278
279 if let Err(ref e) = result {
280 eprintln!("[SqliteGeneStorePersistAdapter] persist_gene error: {e}");
281 }
282 result.is_ok()
283 }
284
285 fn mark_reused(&self, gene_id: &str, capsule_ids: &[String]) -> bool {
286 use oris_genestore::GeneStore;
287 use uuid::Uuid;
288
289 let id = match Uuid::parse_str(gene_id) {
290 Ok(u) => u,
291 Err(_) => return false,
292 };
293
294 let store = &self.store;
295 let result = match tokio::runtime::Handle::try_current() {
296 Ok(handle) => std::thread::scope(|s| {
297 s.spawn(|| handle.block_on(store.record_gene_outcome(id, true)))
298 .join()
299 .unwrap_or_else(|_| Err(anyhow::anyhow!("thread panicked")))
300 }),
301 Err(_) => tokio::runtime::Runtime::new()
302 .map_err(|e| anyhow::anyhow!(e))
303 .and_then(|rt| rt.block_on(store.record_gene_outcome(id, true))),
304 };
305
306 if !capsule_ids.is_empty() {
309 eprintln!(
310 "[SqliteGeneStorePersistAdapter] mark_reused gene={} capsules={:?}",
311 gene_id, capsule_ids
312 );
313 }
314
315 if let Err(ref e) = result {
316 eprintln!("[SqliteGeneStorePersistAdapter] mark_reused error: {e}");
317 }
318 result.is_ok()
319 }
320}
321
322pub struct SandboxOutputValidateAdapter;
334
335impl SandboxOutputValidateAdapter {
336 const FAIL_TOKENS: &'static [&'static str] = &[
338 "FAILED",
339 "error[E",
340 "error:",
341 "panicked at",
342 "thread '",
343 "COMPILATION FAILED",
344 "test result: FAILED",
345 ];
346
347 pub fn new() -> Self {
348 Self
349 }
350}
351
352impl Default for SandboxOutputValidateAdapter {
353 fn default() -> Self {
354 Self::new()
355 }
356}
357
358impl ValidatePort for SandboxOutputValidateAdapter {
359 fn validate(&self, input: &ValidateInput) -> ValidationResult {
360 if input.execution_success {
361 return ValidationResult {
362 proposal_id: input.proposal_id.clone(),
363 passed: true,
364 score: 0.9,
365 issues: vec![],
366 simulation_results: None,
367 };
368 }
369
370 let matching_tokens: Vec<&str> = Self::FAIL_TOKENS
372 .iter()
373 .copied()
374 .filter(|&tok| input.stderr.contains(tok) || input.stdout.contains(tok))
375 .collect();
376
377 let (score, description) = if !matching_tokens.is_empty() {
378 (
379 0.0_f32,
380 format!(
381 "Sandbox execution failed (matched tokens: {}). stderr snippet: {}",
382 matching_tokens.join(", "),
383 &input.stderr[..input.stderr.len().min(200)],
384 ),
385 )
386 } else {
387 (
388 0.2_f32,
389 format!(
390 "Sandbox execution failed without a recognised pattern. stderr snippet: {}",
391 &input.stderr[..input.stderr.len().min(200)],
392 ),
393 )
394 };
395
396 ValidationResult {
397 proposal_id: input.proposal_id.clone(),
398 passed: false,
399 score,
400 issues: vec![ValidationIssue {
401 severity: IssueSeverity::Error,
402 description,
403 location: None,
404 }],
405 simulation_results: None,
406 }
407 }
408}
409
410pub struct MutationEvaluatorAdapter {
419 evaluator: oris_mutation_evaluator::MutationEvaluator,
420}
421
422impl MutationEvaluatorAdapter {
423 pub fn new(evaluator: oris_mutation_evaluator::MutationEvaluator) -> Self {
426 Self { evaluator }
427 }
428
429 pub fn from_mock() -> Self {
433 let backend = oris_mutation_evaluator::MockCritic::passing();
434 Self::new(oris_mutation_evaluator::MutationEvaluator::new(backend))
435 }
436}
437
438impl EvaluatePort for MutationEvaluatorAdapter {
439 fn evaluate(&self, input: &EvaluateInput) -> EvaluationResult {
440 use oris_mutation_evaluator::types::{
441 EvoSignal, MutationProposal, SignalKind as EvalSignalKind,
442 };
443 use uuid::Uuid;
444
445 let signals: Vec<EvoSignal> = input
447 .signals
448 .iter()
449 .map(|s| EvoSignal {
450 kind: EvalSignalKind::CompilerError,
451 message: s.clone(),
452 location: None,
453 })
454 .collect();
455
456 let id = Uuid::parse_str(&input.proposal_id).unwrap_or_else(|_| Uuid::new_v4());
457 let proposal = MutationProposal {
458 id,
459 intent: input.intent.clone(),
460 original: input.original.clone(),
461 proposed: input.proposed.clone(),
462 signals,
463 source_gene_id: None,
464 };
465
466 let evaluator = &self.evaluator;
468 let report_result = match tokio::runtime::Handle::try_current() {
469 Ok(handle) => std::thread::scope(|s| {
470 s.spawn(|| handle.block_on(evaluator.evaluate(&proposal)))
471 .join()
472 .unwrap_or_else(|_| Err(anyhow::anyhow!("evaluator thread panicked")))
473 }),
474 Err(_) => tokio::runtime::Runtime::new()
475 .map_err(|e| anyhow::anyhow!(e))
476 .and_then(|rt| rt.block_on(evaluator.evaluate(&proposal))),
477 };
478
479 match report_result {
480 Ok(report) => {
481 let recommendation = match report.verdict {
482 oris_mutation_evaluator::Verdict::Reject => EvaluationRecommendation::Reject,
483 oris_mutation_evaluator::Verdict::Promote => EvaluationRecommendation::Accept,
484 oris_mutation_evaluator::Verdict::ApplyOnly => EvaluationRecommendation::Accept,
485 };
486 EvaluationResult {
487 score: report.composite_score as f32,
488 improvements: vec![report.rationale.clone()],
489 regressions: report
490 .anti_patterns
491 .iter()
492 .map(|ap| ap.description.clone())
493 .collect(),
494 recommendation,
495 }
496 }
497 Err(e) => {
498 eprintln!("[MutationEvaluatorAdapter] evaluate error: {e}");
499 EvaluationResult {
501 score: 0.0,
502 improvements: vec![],
503 regressions: vec![format!("Evaluator error: {e}")],
504 recommendation: EvaluationRecommendation::RequiresHumanReview,
505 }
506 }
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use oris_mutation_evaluator::{MockCritic, MutationEvaluator};
515
516 fn sample_evaluate_input() -> EvaluateInput {
517 EvaluateInput {
518 proposal_id: "11111111-1111-1111-1111-111111111111".to_string(),
519 intent: "Fix compiler error in docs helper".to_string(),
520 original: "fn helper() -> i32 { broken_call() }".to_string(),
521 proposed: "fn helper() -> i32 { 42 }".to_string(),
522 signals: vec!["cannot find function `broken_call` in this scope".to_string()],
523 }
524 }
525
526 #[test]
527 fn validate_passes_on_success_with_clean_output() {
528 let adapter = SandboxOutputValidateAdapter::new();
529 let result = adapter.validate(&ValidateInput {
530 proposal_id: "proposal-clean".to_string(),
531 execution_success: true,
532 stdout: String::new(),
533 stderr: String::new(),
534 });
535
536 assert!(result.passed);
537 assert_eq!(result.score, 0.9);
538 assert!(result.issues.is_empty());
539 }
540
541 #[test]
542 fn validate_fails_on_execution_failure_with_fail_token() {
543 let adapter = SandboxOutputValidateAdapter::new();
544 let result = adapter.validate(&ValidateInput {
545 proposal_id: "proposal-failed".to_string(),
546 execution_success: false,
547 stdout: "test result: FAILED. 1 passed; 2 failed".to_string(),
548 stderr: "".to_string(),
549 });
550
551 assert!(!result.passed);
552 assert_eq!(result.score, 0.0);
553 assert_eq!(result.issues.len(), 1);
554 assert!(result.issues[0].description.contains("FAILED"));
555 }
556
557 #[test]
558 fn validate_passes_on_success_even_if_stdout_has_warnings() {
559 let adapter = SandboxOutputValidateAdapter::new();
560 let result = adapter.validate(&ValidateInput {
561 proposal_id: "proposal-warn".to_string(),
562 execution_success: true,
563 stdout: "warning: unused import\nwarning: dead_code".to_string(),
564 stderr: String::new(),
565 });
566
567 assert!(result.passed);
568 assert_eq!(result.score, 0.9);
569 assert!(result.issues.is_empty());
570 }
571
572 #[test]
573 fn evaluator_adapter_from_mock_returns_accept() {
574 let adapter = MutationEvaluatorAdapter::from_mock();
575 let result = adapter.evaluate(&sample_evaluate_input());
576
577 assert_eq!(result.recommendation, EvaluationRecommendation::Accept);
578 assert!(result.score > 0.0);
579 assert_eq!(result.improvements.len(), 1);
580 assert!(result.regressions.is_empty());
581 }
582
583 #[test]
584 fn evaluator_adapter_maps_reject_to_reject() {
585 let adapter = MutationEvaluatorAdapter::new(MutationEvaluator::new(MockCritic::failing()));
586 let result = adapter.evaluate(&sample_evaluate_input());
587
588 assert_eq!(result.recommendation, EvaluationRecommendation::Reject);
589 assert!(result.score >= 0.0);
590 }
591}