Skip to main content

oris_evokernel/
core.rs

1//! EvoKernel orchestration: mutation capture, validation, capsule construction, and replay-first reuse.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs;
5use std::path::Path;
6use std::process::Command;
7use std::sync::{Arc, Mutex};
8
9use async_trait::async_trait;
10use chrono::{DateTime, Duration, Utc};
11use oris_agent_contract::{ExecutionFeedback, MutationProposal as AgentMutationProposal};
12use oris_economics::{EconomicsSignal, EvuLedger, StakePolicy};
13use oris_evolution::{
14    compute_artifact_hash, next_id, stable_hash_json, AssetState, BlastRadius, CandidateSource,
15    Capsule, CapsuleId, EnvFingerprint, EvolutionError, EvolutionEvent, EvolutionProjection,
16    EvolutionStore, Gene, GeneCandidate, MutationId, PreparedMutation, Selector, SelectorInput,
17    StoreBackedSelector, StoredEvolutionEvent, ValidationSnapshot,
18};
19use oris_evolution_network::{EvolutionEnvelope, NetworkAsset};
20use oris_governor::{DefaultGovernor, Governor, GovernorDecision, GovernorInput};
21use oris_kernel::{Kernel, KernelState, RunId};
22use oris_sandbox::{
23    compute_blast_radius, execute_allowed_command, Sandbox, SandboxPolicy, SandboxReceipt,
24};
25use oris_spec::CompiledMutationPlan;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28
29pub use oris_evolution::{
30    default_store_root, ArtifactEncoding, AssetState as EvoAssetState,
31    BlastRadius as EvoBlastRadius, CandidateSource as EvoCandidateSource,
32    EnvFingerprint as EvoEnvFingerprint, EvolutionStore as EvoEvolutionStore, JsonlEvolutionStore,
33    MutationArtifact, MutationIntent, MutationTarget, Outcome, RiskLevel,
34    SelectorInput as EvoSelectorInput,
35};
36pub use oris_evolution_network::{
37    FetchQuery, FetchResponse, MessageType, PublishRequest, RevokeNotice,
38};
39pub use oris_governor::{CoolingWindow, GovernorConfig, RevocationReason};
40pub use oris_sandbox::{LocalProcessSandbox, SandboxPolicy as EvoSandboxPolicy};
41pub use oris_spec::{SpecCompileError, SpecCompiler, SpecDocument};
42
43#[derive(Clone, Debug, Serialize, Deserialize)]
44pub struct ValidationPlan {
45    pub profile: String,
46    pub stages: Vec<ValidationStage>,
47}
48
49impl ValidationPlan {
50    pub fn oris_default() -> Self {
51        Self {
52            profile: "oris-default".into(),
53            stages: vec![
54                ValidationStage::Command {
55                    program: "cargo".into(),
56                    args: vec!["fmt".into(), "--all".into(), "--check".into()],
57                    timeout_ms: 60_000,
58                },
59                ValidationStage::Command {
60                    program: "cargo".into(),
61                    args: vec!["check".into(), "--workspace".into()],
62                    timeout_ms: 180_000,
63                },
64                ValidationStage::Command {
65                    program: "cargo".into(),
66                    args: vec![
67                        "test".into(),
68                        "-p".into(),
69                        "oris-kernel".into(),
70                        "-p".into(),
71                        "oris-evolution".into(),
72                        "-p".into(),
73                        "oris-sandbox".into(),
74                        "-p".into(),
75                        "oris-evokernel".into(),
76                        "--lib".into(),
77                    ],
78                    timeout_ms: 300_000,
79                },
80                ValidationStage::Command {
81                    program: "cargo".into(),
82                    args: vec![
83                        "test".into(),
84                        "-p".into(),
85                        "oris-runtime".into(),
86                        "--lib".into(),
87                    ],
88                    timeout_ms: 300_000,
89                },
90            ],
91        }
92    }
93}
94
95#[derive(Clone, Debug, Serialize, Deserialize)]
96pub enum ValidationStage {
97    Command {
98        program: String,
99        args: Vec<String>,
100        timeout_ms: u64,
101    },
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize)]
105pub struct ValidationStageReport {
106    pub stage: String,
107    pub success: bool,
108    pub exit_code: Option<i32>,
109    pub duration_ms: u64,
110    pub stdout: String,
111    pub stderr: String,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct ValidationReport {
116    pub success: bool,
117    pub duration_ms: u64,
118    pub stages: Vec<ValidationStageReport>,
119    pub logs: String,
120}
121
122impl ValidationReport {
123    pub fn to_snapshot(&self, profile: &str) -> ValidationSnapshot {
124        ValidationSnapshot {
125            success: self.success,
126            profile: profile.to_string(),
127            duration_ms: self.duration_ms,
128            summary: if self.success {
129                "validation passed".into()
130            } else {
131                "validation failed".into()
132            },
133        }
134    }
135}
136
137#[derive(Debug, Error)]
138pub enum ValidationError {
139    #[error("validation execution failed: {0}")]
140    Execution(String),
141}
142
143#[async_trait]
144pub trait Validator: Send + Sync {
145    async fn run(
146        &self,
147        receipt: &SandboxReceipt,
148        plan: &ValidationPlan,
149    ) -> Result<ValidationReport, ValidationError>;
150}
151
152pub struct CommandValidator {
153    policy: SandboxPolicy,
154}
155
156impl CommandValidator {
157    pub fn new(policy: SandboxPolicy) -> Self {
158        Self { policy }
159    }
160}
161
162#[async_trait]
163impl Validator for CommandValidator {
164    async fn run(
165        &self,
166        receipt: &SandboxReceipt,
167        plan: &ValidationPlan,
168    ) -> Result<ValidationReport, ValidationError> {
169        let started = std::time::Instant::now();
170        let mut stages = Vec::new();
171        let mut success = true;
172        let mut logs = String::new();
173
174        for stage in &plan.stages {
175            match stage {
176                ValidationStage::Command {
177                    program,
178                    args,
179                    timeout_ms,
180                } => {
181                    let result = execute_allowed_command(
182                        &self.policy,
183                        &receipt.workdir,
184                        program,
185                        args,
186                        *timeout_ms,
187                    )
188                    .await;
189                    let report = match result {
190                        Ok(output) => ValidationStageReport {
191                            stage: format!("{program} {}", args.join(" ")),
192                            success: output.success,
193                            exit_code: output.exit_code,
194                            duration_ms: output.duration_ms,
195                            stdout: output.stdout,
196                            stderr: output.stderr,
197                        },
198                        Err(err) => ValidationStageReport {
199                            stage: format!("{program} {}", args.join(" ")),
200                            success: false,
201                            exit_code: None,
202                            duration_ms: 0,
203                            stdout: String::new(),
204                            stderr: err.to_string(),
205                        },
206                    };
207                    if !report.success {
208                        success = false;
209                    }
210                    if !report.stdout.is_empty() {
211                        logs.push_str(&report.stdout);
212                        logs.push('\n');
213                    }
214                    if !report.stderr.is_empty() {
215                        logs.push_str(&report.stderr);
216                        logs.push('\n');
217                    }
218                    stages.push(report);
219                    if !success {
220                        break;
221                    }
222                }
223            }
224        }
225
226        Ok(ValidationReport {
227            success,
228            duration_ms: started.elapsed().as_millis() as u64,
229            stages,
230            logs,
231        })
232    }
233}
234
235#[derive(Clone, Debug)]
236pub struct ReplayDecision {
237    pub used_capsule: bool,
238    pub capsule_id: Option<CapsuleId>,
239    pub fallback_to_planner: bool,
240    pub reason: String,
241}
242
243#[derive(Debug, Error)]
244pub enum ReplayError {
245    #[error("store error: {0}")]
246    Store(String),
247    #[error("sandbox error: {0}")]
248    Sandbox(String),
249    #[error("validation error: {0}")]
250    Validation(String),
251}
252
253#[async_trait]
254pub trait ReplayExecutor: Send + Sync {
255    async fn try_replay(
256        &self,
257        input: &SelectorInput,
258        policy: &SandboxPolicy,
259        validation: &ValidationPlan,
260    ) -> Result<ReplayDecision, ReplayError>;
261}
262
263pub struct StoreReplayExecutor {
264    pub sandbox: Arc<dyn Sandbox>,
265    pub validator: Arc<dyn Validator>,
266    pub store: Arc<dyn EvolutionStore>,
267    pub selector: Arc<dyn Selector>,
268    pub governor: Arc<dyn Governor>,
269    pub economics: Option<Arc<Mutex<EvuLedger>>>,
270    pub remote_publishers: Option<Arc<Mutex<BTreeMap<String, String>>>>,
271    pub stake_policy: StakePolicy,
272}
273
274#[async_trait]
275impl ReplayExecutor for StoreReplayExecutor {
276    async fn try_replay(
277        &self,
278        input: &SelectorInput,
279        policy: &SandboxPolicy,
280        validation: &ValidationPlan,
281    ) -> Result<ReplayDecision, ReplayError> {
282        let mut selector_input = input.clone();
283        if self.economics.is_some() && self.remote_publishers.is_some() {
284            selector_input.limit = selector_input.limit.max(4);
285        }
286        let mut candidates = self.selector.select(&selector_input);
287        self.rerank_with_reputation_bias(&mut candidates);
288        let mut exact_match = false;
289        if candidates.is_empty() {
290            let mut exact_candidates = exact_match_candidates(self.store.as_ref(), input);
291            self.rerank_with_reputation_bias(&mut exact_candidates);
292            if !exact_candidates.is_empty() {
293                candidates = exact_candidates;
294                exact_match = true;
295            }
296        }
297        if candidates.is_empty() {
298            let mut remote_candidates =
299                quarantined_remote_exact_match_candidates(self.store.as_ref(), input);
300            self.rerank_with_reputation_bias(&mut remote_candidates);
301            if !remote_candidates.is_empty() {
302                candidates = remote_candidates;
303                exact_match = true;
304            }
305        }
306        candidates.truncate(input.limit.max(1));
307        let Some(best) = candidates.into_iter().next() else {
308            return Ok(ReplayDecision {
309                used_capsule: false,
310                capsule_id: None,
311                fallback_to_planner: true,
312                reason: "no matching gene".into(),
313            });
314        };
315        let remote_publisher = self.publisher_for_gene(&best.gene.id);
316
317        if !exact_match && best.score < 0.82 {
318            return Ok(ReplayDecision {
319                used_capsule: false,
320                capsule_id: None,
321                fallback_to_planner: true,
322                reason: format!("best gene score {:.3} below replay threshold", best.score),
323            });
324        }
325
326        let Some(capsule) = best.capsules.first().cloned() else {
327            return Ok(ReplayDecision {
328                used_capsule: false,
329                capsule_id: None,
330                fallback_to_planner: true,
331                reason: "candidate gene has no capsule".into(),
332            });
333        };
334
335        let Some(mutation) = find_declared_mutation(self.store.as_ref(), &capsule.mutation_id)
336            .map_err(|err| ReplayError::Store(err.to_string()))?
337        else {
338            return Ok(ReplayDecision {
339                used_capsule: false,
340                capsule_id: None,
341                fallback_to_planner: true,
342                reason: "mutation payload missing from store".into(),
343            });
344        };
345
346        let receipt = match self.sandbox.apply(&mutation, policy).await {
347            Ok(receipt) => receipt,
348            Err(err) => {
349                self.record_reuse_settlement(remote_publisher.as_deref(), false);
350                return Ok(ReplayDecision {
351                    used_capsule: false,
352                    capsule_id: Some(capsule.id.clone()),
353                    fallback_to_planner: true,
354                    reason: format!("replay patch apply failed: {err}"),
355                });
356            }
357        };
358
359        let report = self
360            .validator
361            .run(&receipt, validation)
362            .await
363            .map_err(|err| ReplayError::Validation(err.to_string()))?;
364        if !report.success {
365            self.record_replay_validation_failure(&best, &capsule, validation, &report)?;
366            self.record_reuse_settlement(remote_publisher.as_deref(), false);
367            return Ok(ReplayDecision {
368                used_capsule: false,
369                capsule_id: Some(capsule.id.clone()),
370                fallback_to_planner: true,
371                reason: "replay validation failed".into(),
372            });
373        }
374
375        if matches!(capsule.state, AssetState::Quarantined) {
376            self.store
377                .append_event(EvolutionEvent::ValidationPassed {
378                    mutation_id: capsule.mutation_id.clone(),
379                    report: report.to_snapshot(&validation.profile),
380                    gene_id: Some(best.gene.id.clone()),
381                })
382                .map_err(|err| ReplayError::Store(err.to_string()))?;
383            self.store
384                .append_event(EvolutionEvent::CapsuleReleased {
385                    capsule_id: capsule.id.clone(),
386                    state: AssetState::Promoted,
387                })
388                .map_err(|err| ReplayError::Store(err.to_string()))?;
389        }
390
391        self.store
392            .append_event(EvolutionEvent::CapsuleReused {
393                capsule_id: capsule.id.clone(),
394                gene_id: capsule.gene_id.clone(),
395                run_id: capsule.run_id.clone(),
396            })
397            .map_err(|err| ReplayError::Store(err.to_string()))?;
398        self.record_reuse_settlement(remote_publisher.as_deref(), true);
399
400        Ok(ReplayDecision {
401            used_capsule: true,
402            capsule_id: Some(capsule.id),
403            fallback_to_planner: false,
404            reason: if exact_match {
405                "replayed via exact-match cold-start lookup".into()
406            } else {
407                "replayed via selector".into()
408            },
409        })
410    }
411}
412
413impl StoreReplayExecutor {
414    fn rerank_with_reputation_bias(&self, candidates: &mut [GeneCandidate]) {
415        let Some(ledger) = self.economics.as_ref() else {
416            return;
417        };
418        let Some(remote_publishers) = self.remote_publishers.as_ref() else {
419            return;
420        };
421        let reputation_bias = ledger
422            .lock()
423            .ok()
424            .map(|locked| locked.selector_reputation_bias())
425            .unwrap_or_default();
426        if reputation_bias.is_empty() {
427            return;
428        }
429        let publisher_map = remote_publishers
430            .lock()
431            .ok()
432            .map(|locked| locked.clone())
433            .unwrap_or_default();
434        candidates.sort_by(|left, right| {
435            effective_candidate_score(right, &publisher_map, &reputation_bias)
436                .partial_cmp(&effective_candidate_score(
437                    left,
438                    &publisher_map,
439                    &reputation_bias,
440                ))
441                .unwrap_or(std::cmp::Ordering::Equal)
442                .then_with(|| left.gene.id.cmp(&right.gene.id))
443        });
444    }
445
446    fn publisher_for_gene(&self, gene_id: &str) -> Option<String> {
447        self.remote_publishers
448            .as_ref()?
449            .lock()
450            .ok()?
451            .get(gene_id)
452            .cloned()
453    }
454
455    fn record_reuse_settlement(&self, publisher_id: Option<&str>, success: bool) {
456        let Some(publisher_id) = publisher_id else {
457            return;
458        };
459        let Some(ledger) = self.economics.as_ref() else {
460            return;
461        };
462        if let Ok(mut locked) = ledger.lock() {
463            locked.settle_remote_reuse(publisher_id, success, &self.stake_policy);
464        }
465    }
466
467    fn record_replay_validation_failure(
468        &self,
469        best: &GeneCandidate,
470        capsule: &Capsule,
471        validation: &ValidationPlan,
472        report: &ValidationReport,
473    ) -> Result<(), ReplayError> {
474        let projection = self
475            .store
476            .rebuild_projection()
477            .map_err(|err| ReplayError::Store(err.to_string()))?;
478        let (current_confidence, historical_peak_confidence, confidence_last_updated_secs) =
479            Self::confidence_context(&projection, &best.gene.id);
480
481        self.store
482            .append_event(EvolutionEvent::ValidationFailed {
483                mutation_id: capsule.mutation_id.clone(),
484                report: report.to_snapshot(&validation.profile),
485                gene_id: Some(best.gene.id.clone()),
486            })
487            .map_err(|err| ReplayError::Store(err.to_string()))?;
488
489        let replay_failures = self.replay_failure_count(&best.gene.id)?;
490        let governor_decision = self.governor.evaluate(GovernorInput {
491            candidate_source: if self.publisher_for_gene(&best.gene.id).is_some() {
492                CandidateSource::Remote
493            } else {
494                CandidateSource::Local
495            },
496            success_count: 0,
497            blast_radius: BlastRadius {
498                files_changed: capsule.outcome.changed_files.len(),
499                lines_changed: capsule.outcome.lines_changed,
500            },
501            replay_failures,
502            recent_mutation_ages_secs: Vec::new(),
503            current_confidence,
504            historical_peak_confidence,
505            confidence_last_updated_secs,
506        });
507
508        if matches!(governor_decision.target_state, AssetState::Revoked) {
509            self.store
510                .append_event(EvolutionEvent::PromotionEvaluated {
511                    gene_id: best.gene.id.clone(),
512                    state: AssetState::Revoked,
513                    reason: governor_decision.reason.clone(),
514                })
515                .map_err(|err| ReplayError::Store(err.to_string()))?;
516            self.store
517                .append_event(EvolutionEvent::GeneRevoked {
518                    gene_id: best.gene.id.clone(),
519                    reason: governor_decision.reason,
520                })
521                .map_err(|err| ReplayError::Store(err.to_string()))?;
522            for related in &best.capsules {
523                self.store
524                    .append_event(EvolutionEvent::CapsuleQuarantined {
525                        capsule_id: related.id.clone(),
526                    })
527                    .map_err(|err| ReplayError::Store(err.to_string()))?;
528            }
529        }
530
531        Ok(())
532    }
533
534    fn confidence_context(
535        projection: &EvolutionProjection,
536        gene_id: &str,
537    ) -> (f32, f32, Option<u64>) {
538        let peak_confidence = projection
539            .capsules
540            .iter()
541            .filter(|capsule| capsule.gene_id == gene_id)
542            .map(|capsule| capsule.confidence)
543            .fold(0.0_f32, f32::max);
544        let age_secs = projection
545            .last_updated_at
546            .get(gene_id)
547            .and_then(|timestamp| Self::seconds_since_timestamp(timestamp, Utc::now()));
548        (peak_confidence, peak_confidence, age_secs)
549    }
550
551    fn seconds_since_timestamp(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
552        let parsed = DateTime::parse_from_rfc3339(timestamp)
553            .ok()?
554            .with_timezone(&Utc);
555        let elapsed = now.signed_duration_since(parsed);
556        if elapsed < Duration::zero() {
557            Some(0)
558        } else {
559            u64::try_from(elapsed.num_seconds()).ok()
560        }
561    }
562
563    fn replay_failure_count(&self, gene_id: &str) -> Result<u64, ReplayError> {
564        Ok(self
565            .store
566            .scan(1)
567            .map_err(|err| ReplayError::Store(err.to_string()))?
568            .into_iter()
569            .filter(|stored| {
570                matches!(
571                    &stored.event,
572                    EvolutionEvent::ValidationFailed {
573                        gene_id: Some(current_gene_id),
574                        ..
575                    } if current_gene_id == gene_id
576                )
577            })
578            .count() as u64)
579    }
580}
581
582#[derive(Debug, Error)]
583pub enum EvoKernelError {
584    #[error("sandbox error: {0}")]
585    Sandbox(String),
586    #[error("validation error: {0}")]
587    Validation(String),
588    #[error("validation failed")]
589    ValidationFailed(ValidationReport),
590    #[error("store error: {0}")]
591    Store(String),
592}
593
594#[derive(Clone, Debug)]
595pub struct CaptureOutcome {
596    pub capsule: Capsule,
597    pub gene: Gene,
598    pub governor_decision: GovernorDecision,
599}
600
601#[derive(Clone, Debug, Serialize, Deserialize)]
602pub struct ImportOutcome {
603    pub imported_asset_ids: Vec<String>,
604    pub accepted: bool,
605}
606
607#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
608pub struct EvolutionMetricsSnapshot {
609    pub replay_attempts_total: u64,
610    pub replay_success_total: u64,
611    pub replay_success_rate: f64,
612    pub mutation_declared_total: u64,
613    pub promoted_mutations_total: u64,
614    pub promotion_ratio: f64,
615    pub gene_revocations_total: u64,
616    pub mutation_velocity_last_hour: u64,
617    pub revoke_frequency_last_hour: u64,
618    pub promoted_genes: u64,
619    pub promoted_capsules: u64,
620    pub last_event_seq: u64,
621}
622
623#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
624pub struct EvolutionHealthSnapshot {
625    pub status: String,
626    pub last_event_seq: u64,
627    pub promoted_genes: u64,
628    pub promoted_capsules: u64,
629}
630
631#[derive(Clone)]
632pub struct EvolutionNetworkNode {
633    pub store: Arc<dyn EvolutionStore>,
634}
635
636impl EvolutionNetworkNode {
637    pub fn new(store: Arc<dyn EvolutionStore>) -> Self {
638        Self { store }
639    }
640
641    pub fn with_default_store() -> Self {
642        Self {
643            store: Arc::new(JsonlEvolutionStore::new(default_store_root())),
644        }
645    }
646
647    pub fn accept_publish_request(
648        &self,
649        request: &PublishRequest,
650    ) -> Result<ImportOutcome, EvoKernelError> {
651        import_remote_envelope_into_store(
652            self.store.as_ref(),
653            &EvolutionEnvelope::publish(request.sender_id.clone(), request.assets.clone()),
654        )
655    }
656
657    pub fn publish_local_assets(
658        &self,
659        sender_id: impl Into<String>,
660    ) -> Result<EvolutionEnvelope, EvoKernelError> {
661        export_promoted_assets_from_store(self.store.as_ref(), sender_id)
662    }
663
664    pub fn fetch_assets(
665        &self,
666        responder_id: impl Into<String>,
667        query: &FetchQuery,
668    ) -> Result<FetchResponse, EvoKernelError> {
669        fetch_assets_from_store(self.store.as_ref(), responder_id, query)
670    }
671
672    pub fn revoke_assets(&self, notice: &RevokeNotice) -> Result<RevokeNotice, EvoKernelError> {
673        revoke_assets_in_store(self.store.as_ref(), notice)
674    }
675
676    pub fn metrics_snapshot(&self) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
677        evolution_metrics_snapshot(self.store.as_ref())
678    }
679
680    pub fn render_metrics_prometheus(&self) -> Result<String, EvoKernelError> {
681        self.metrics_snapshot().map(|snapshot| {
682            let health = evolution_health_snapshot(&snapshot);
683            render_evolution_metrics_prometheus(&snapshot, &health)
684        })
685    }
686
687    pub fn health_snapshot(&self) -> Result<EvolutionHealthSnapshot, EvoKernelError> {
688        self.metrics_snapshot()
689            .map(|snapshot| evolution_health_snapshot(&snapshot))
690    }
691}
692
693pub struct EvoKernel<S: KernelState> {
694    pub kernel: Arc<Kernel<S>>,
695    pub sandbox: Arc<dyn Sandbox>,
696    pub validator: Arc<dyn Validator>,
697    pub store: Arc<dyn EvolutionStore>,
698    pub selector: Arc<dyn Selector>,
699    pub governor: Arc<dyn Governor>,
700    pub economics: Arc<Mutex<EvuLedger>>,
701    pub remote_publishers: Arc<Mutex<BTreeMap<String, String>>>,
702    pub stake_policy: StakePolicy,
703    pub sandbox_policy: SandboxPolicy,
704    pub validation_plan: ValidationPlan,
705}
706
707impl<S: KernelState> EvoKernel<S> {
708    fn recent_prior_mutation_ages_secs(
709        &self,
710        exclude_mutation_id: Option<&str>,
711    ) -> Result<Vec<u64>, EvolutionError> {
712        let now = Utc::now();
713        let mut ages = self
714            .store
715            .scan(1)?
716            .into_iter()
717            .filter_map(|stored| match stored.event {
718                EvolutionEvent::MutationDeclared { mutation }
719                    if exclude_mutation_id != Some(mutation.intent.id.as_str()) =>
720                {
721                    Self::seconds_since_timestamp(&stored.timestamp, now)
722                }
723                _ => None,
724            })
725            .collect::<Vec<_>>();
726        ages.sort_unstable();
727        Ok(ages)
728    }
729
730    fn seconds_since_timestamp(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
731        let parsed = DateTime::parse_from_rfc3339(timestamp)
732            .ok()?
733            .with_timezone(&Utc);
734        let elapsed = now.signed_duration_since(parsed);
735        if elapsed < Duration::zero() {
736            Some(0)
737        } else {
738            u64::try_from(elapsed.num_seconds()).ok()
739        }
740    }
741
742    pub fn new(
743        kernel: Arc<Kernel<S>>,
744        sandbox: Arc<dyn Sandbox>,
745        validator: Arc<dyn Validator>,
746        store: Arc<dyn EvolutionStore>,
747    ) -> Self {
748        let selector: Arc<dyn Selector> = Arc::new(StoreBackedSelector::new(store.clone()));
749        Self {
750            kernel,
751            sandbox,
752            validator,
753            store,
754            selector,
755            governor: Arc::new(DefaultGovernor::default()),
756            economics: Arc::new(Mutex::new(EvuLedger::default())),
757            remote_publishers: Arc::new(Mutex::new(BTreeMap::new())),
758            stake_policy: StakePolicy::default(),
759            sandbox_policy: SandboxPolicy::oris_default(),
760            validation_plan: ValidationPlan::oris_default(),
761        }
762    }
763
764    pub fn with_selector(mut self, selector: Arc<dyn Selector>) -> Self {
765        self.selector = selector;
766        self
767    }
768
769    pub fn with_sandbox_policy(mut self, policy: SandboxPolicy) -> Self {
770        self.sandbox_policy = policy;
771        self
772    }
773
774    pub fn with_governor(mut self, governor: Arc<dyn Governor>) -> Self {
775        self.governor = governor;
776        self
777    }
778
779    pub fn with_economics(mut self, economics: Arc<Mutex<EvuLedger>>) -> Self {
780        self.economics = economics;
781        self
782    }
783
784    pub fn with_stake_policy(mut self, policy: StakePolicy) -> Self {
785        self.stake_policy = policy;
786        self
787    }
788
789    pub fn with_validation_plan(mut self, plan: ValidationPlan) -> Self {
790        self.validation_plan = plan;
791        self
792    }
793
794    pub async fn capture_successful_mutation(
795        &self,
796        run_id: &RunId,
797        mutation: PreparedMutation,
798    ) -> Result<Capsule, EvoKernelError> {
799        Ok(self
800            .capture_mutation_with_governor(run_id, mutation)
801            .await?
802            .capsule)
803    }
804
805    pub async fn capture_mutation_with_governor(
806        &self,
807        run_id: &RunId,
808        mutation: PreparedMutation,
809    ) -> Result<CaptureOutcome, EvoKernelError> {
810        self.store
811            .append_event(EvolutionEvent::MutationDeclared {
812                mutation: mutation.clone(),
813            })
814            .map_err(store_err)?;
815
816        let receipt = match self.sandbox.apply(&mutation, &self.sandbox_policy).await {
817            Ok(receipt) => receipt,
818            Err(err) => {
819                self.store
820                    .append_event(EvolutionEvent::MutationRejected {
821                        mutation_id: mutation.intent.id.clone(),
822                        reason: err.to_string(),
823                    })
824                    .map_err(store_err)?;
825                return Err(EvoKernelError::Sandbox(err.to_string()));
826            }
827        };
828
829        self.store
830            .append_event(EvolutionEvent::MutationApplied {
831                mutation_id: mutation.intent.id.clone(),
832                patch_hash: receipt.patch_hash.clone(),
833                changed_files: receipt
834                    .changed_files
835                    .iter()
836                    .map(|path| path.to_string_lossy().to_string())
837                    .collect(),
838            })
839            .map_err(store_err)?;
840
841        let report = self
842            .validator
843            .run(&receipt, &self.validation_plan)
844            .await
845            .map_err(|err| EvoKernelError::Validation(err.to_string()))?;
846        if !report.success {
847            self.store
848                .append_event(EvolutionEvent::ValidationFailed {
849                    mutation_id: mutation.intent.id.clone(),
850                    report: report.to_snapshot(&self.validation_plan.profile),
851                    gene_id: None,
852                })
853                .map_err(store_err)?;
854            return Err(EvoKernelError::ValidationFailed(report));
855        }
856
857        let projection = self.store.rebuild_projection().map_err(store_err)?;
858        let blast_radius = compute_blast_radius(&mutation.artifact.payload);
859        let recent_mutation_ages_secs = self
860            .recent_prior_mutation_ages_secs(Some(mutation.intent.id.as_str()))
861            .map_err(store_err)?;
862        let mut gene = derive_gene(&mutation, &receipt, &self.validation_plan.profile);
863        let success_count = projection
864            .genes
865            .iter()
866            .find(|existing| existing.id == gene.id)
867            .map(|existing| {
868                projection
869                    .capsules
870                    .iter()
871                    .filter(|capsule| capsule.gene_id == existing.id)
872                    .count() as u64
873            })
874            .unwrap_or(0)
875            + 1;
876        let governor_decision = self.governor.evaluate(GovernorInput {
877            candidate_source: CandidateSource::Local,
878            success_count,
879            blast_radius: blast_radius.clone(),
880            replay_failures: 0,
881            recent_mutation_ages_secs,
882            current_confidence: 0.7,
883            historical_peak_confidence: 0.7,
884            confidence_last_updated_secs: Some(0),
885        });
886
887        gene.state = governor_decision.target_state.clone();
888        self.store
889            .append_event(EvolutionEvent::ValidationPassed {
890                mutation_id: mutation.intent.id.clone(),
891                report: report.to_snapshot(&self.validation_plan.profile),
892                gene_id: Some(gene.id.clone()),
893            })
894            .map_err(store_err)?;
895        self.store
896            .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
897            .map_err(store_err)?;
898        self.store
899            .append_event(EvolutionEvent::PromotionEvaluated {
900                gene_id: gene.id.clone(),
901                state: governor_decision.target_state.clone(),
902                reason: governor_decision.reason.clone(),
903            })
904            .map_err(store_err)?;
905        if matches!(governor_decision.target_state, AssetState::Promoted) {
906            self.store
907                .append_event(EvolutionEvent::GenePromoted {
908                    gene_id: gene.id.clone(),
909                })
910                .map_err(store_err)?;
911        }
912        if let Some(spec_id) = &mutation.intent.spec_id {
913            self.store
914                .append_event(EvolutionEvent::SpecLinked {
915                    mutation_id: mutation.intent.id.clone(),
916                    spec_id: spec_id.clone(),
917                })
918                .map_err(store_err)?;
919        }
920
921        let mut capsule = build_capsule(
922            run_id,
923            &mutation,
924            &receipt,
925            &report,
926            &self.validation_plan.profile,
927            &gene,
928            &blast_radius,
929        )
930        .map_err(|err| EvoKernelError::Validation(err.to_string()))?;
931        capsule.state = governor_decision.target_state.clone();
932        self.store
933            .append_event(EvolutionEvent::CapsuleCommitted {
934                capsule: capsule.clone(),
935            })
936            .map_err(store_err)?;
937        if matches!(governor_decision.target_state, AssetState::Quarantined) {
938            self.store
939                .append_event(EvolutionEvent::CapsuleQuarantined {
940                    capsule_id: capsule.id.clone(),
941                })
942                .map_err(store_err)?;
943        }
944
945        Ok(CaptureOutcome {
946            capsule,
947            gene,
948            governor_decision,
949        })
950    }
951
952    pub async fn capture_from_proposal(
953        &self,
954        run_id: &RunId,
955        proposal: &AgentMutationProposal,
956        diff_payload: String,
957        base_revision: Option<String>,
958    ) -> Result<CaptureOutcome, EvoKernelError> {
959        let intent = MutationIntent {
960            id: next_id("proposal"),
961            intent: proposal.intent.clone(),
962            target: MutationTarget::Paths {
963                allow: proposal.files.clone(),
964            },
965            expected_effect: proposal.expected_effect.clone(),
966            risk: RiskLevel::Low,
967            signals: proposal.files.clone(),
968            spec_id: None,
969        };
970        self.capture_mutation_with_governor(
971            run_id,
972            prepare_mutation(intent, diff_payload, base_revision),
973        )
974        .await
975    }
976
977    pub fn feedback_for_agent(outcome: &CaptureOutcome) -> ExecutionFeedback {
978        ExecutionFeedback {
979            accepted: !matches!(outcome.governor_decision.target_state, AssetState::Revoked),
980            asset_state: Some(format!("{:?}", outcome.governor_decision.target_state)),
981            summary: outcome.governor_decision.reason.clone(),
982        }
983    }
984
985    pub fn export_promoted_assets(
986        &self,
987        sender_id: impl Into<String>,
988    ) -> Result<EvolutionEnvelope, EvoKernelError> {
989        let sender_id = sender_id.into();
990        let envelope = export_promoted_assets_from_store(self.store.as_ref(), sender_id.clone())?;
991        if !envelope.assets.is_empty() {
992            let mut ledger = self
993                .economics
994                .lock()
995                .map_err(|_| EvoKernelError::Validation("economics ledger lock poisoned".into()))?;
996            if ledger
997                .reserve_publish_stake(&sender_id, &self.stake_policy)
998                .is_none()
999            {
1000                return Err(EvoKernelError::Validation(
1001                    "insufficient EVU for remote publish".into(),
1002                ));
1003            }
1004        }
1005        Ok(envelope)
1006    }
1007
1008    pub fn import_remote_envelope(
1009        &self,
1010        envelope: &EvolutionEnvelope,
1011    ) -> Result<ImportOutcome, EvoKernelError> {
1012        let outcome = import_remote_envelope_into_store(self.store.as_ref(), envelope)?;
1013        self.record_remote_publishers(envelope);
1014        Ok(outcome)
1015    }
1016
1017    pub fn fetch_assets(
1018        &self,
1019        responder_id: impl Into<String>,
1020        query: &FetchQuery,
1021    ) -> Result<FetchResponse, EvoKernelError> {
1022        fetch_assets_from_store(self.store.as_ref(), responder_id, query)
1023    }
1024
1025    pub fn revoke_assets(&self, notice: &RevokeNotice) -> Result<RevokeNotice, EvoKernelError> {
1026        revoke_assets_in_store(self.store.as_ref(), notice)
1027    }
1028
1029    pub async fn replay_or_fallback(
1030        &self,
1031        input: SelectorInput,
1032    ) -> Result<ReplayDecision, EvoKernelError> {
1033        let executor = StoreReplayExecutor {
1034            sandbox: self.sandbox.clone(),
1035            validator: self.validator.clone(),
1036            store: self.store.clone(),
1037            selector: self.selector.clone(),
1038            governor: self.governor.clone(),
1039            economics: Some(self.economics.clone()),
1040            remote_publishers: Some(self.remote_publishers.clone()),
1041            stake_policy: self.stake_policy.clone(),
1042        };
1043        executor
1044            .try_replay(&input, &self.sandbox_policy, &self.validation_plan)
1045            .await
1046            .map_err(|err| EvoKernelError::Validation(err.to_string()))
1047    }
1048
1049    pub fn economics_signal(&self, node_id: &str) -> Option<EconomicsSignal> {
1050        self.economics.lock().ok()?.governor_signal(node_id)
1051    }
1052
1053    pub fn selector_reputation_bias(&self) -> BTreeMap<String, f32> {
1054        self.economics
1055            .lock()
1056            .ok()
1057            .map(|locked| locked.selector_reputation_bias())
1058            .unwrap_or_default()
1059    }
1060
1061    pub fn metrics_snapshot(&self) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
1062        evolution_metrics_snapshot(self.store.as_ref())
1063    }
1064
1065    pub fn render_metrics_prometheus(&self) -> Result<String, EvoKernelError> {
1066        self.metrics_snapshot().map(|snapshot| {
1067            let health = evolution_health_snapshot(&snapshot);
1068            render_evolution_metrics_prometheus(&snapshot, &health)
1069        })
1070    }
1071
1072    pub fn health_snapshot(&self) -> Result<EvolutionHealthSnapshot, EvoKernelError> {
1073        self.metrics_snapshot()
1074            .map(|snapshot| evolution_health_snapshot(&snapshot))
1075    }
1076
1077    fn record_remote_publishers(&self, envelope: &EvolutionEnvelope) {
1078        let sender_id = envelope.sender_id.trim();
1079        if sender_id.is_empty() {
1080            return;
1081        }
1082        let Ok(mut publishers) = self.remote_publishers.lock() else {
1083            return;
1084        };
1085        for asset in &envelope.assets {
1086            match asset {
1087                NetworkAsset::Gene { gene } => {
1088                    publishers.insert(gene.id.clone(), sender_id.to_string());
1089                }
1090                NetworkAsset::Capsule { capsule } => {
1091                    publishers.insert(capsule.gene_id.clone(), sender_id.to_string());
1092                }
1093                NetworkAsset::EvolutionEvent { .. } => {}
1094            }
1095        }
1096    }
1097}
1098
1099pub fn prepare_mutation(
1100    intent: MutationIntent,
1101    diff_payload: String,
1102    base_revision: Option<String>,
1103) -> PreparedMutation {
1104    PreparedMutation {
1105        intent,
1106        artifact: MutationArtifact {
1107            encoding: ArtifactEncoding::UnifiedDiff,
1108            content_hash: compute_artifact_hash(&diff_payload),
1109            payload: diff_payload,
1110            base_revision,
1111        },
1112    }
1113}
1114
1115pub fn prepare_mutation_from_spec(
1116    plan: CompiledMutationPlan,
1117    diff_payload: String,
1118    base_revision: Option<String>,
1119) -> PreparedMutation {
1120    prepare_mutation(plan.mutation_intent, diff_payload, base_revision)
1121}
1122
1123pub fn default_evolution_store() -> Arc<dyn EvolutionStore> {
1124    Arc::new(oris_evolution::JsonlEvolutionStore::new(
1125        default_store_root(),
1126    ))
1127}
1128
1129fn derive_gene(
1130    mutation: &PreparedMutation,
1131    receipt: &SandboxReceipt,
1132    validation_profile: &str,
1133) -> Gene {
1134    let mut strategy = BTreeSet::new();
1135    for file in &receipt.changed_files {
1136        if let Some(component) = file.components().next() {
1137            strategy.insert(component.as_os_str().to_string_lossy().to_string());
1138        }
1139    }
1140    for token in mutation
1141        .artifact
1142        .payload
1143        .split(|ch: char| !ch.is_ascii_alphanumeric())
1144    {
1145        if token.len() == 5
1146            && token.starts_with('E')
1147            && token[1..].chars().all(|ch| ch.is_ascii_digit())
1148        {
1149            strategy.insert(token.to_string());
1150        }
1151    }
1152    for token in mutation.intent.intent.split_whitespace().take(8) {
1153        strategy.insert(token.to_ascii_lowercase());
1154    }
1155    let strategy = strategy.into_iter().collect::<Vec<_>>();
1156    let id = stable_hash_json(&(&mutation.intent.signals, &strategy, validation_profile))
1157        .unwrap_or_else(|_| next_id("gene"));
1158    Gene {
1159        id,
1160        signals: mutation.intent.signals.clone(),
1161        strategy,
1162        validation: vec![validation_profile.to_string()],
1163        state: AssetState::Promoted,
1164    }
1165}
1166
1167fn build_capsule(
1168    run_id: &RunId,
1169    mutation: &PreparedMutation,
1170    receipt: &SandboxReceipt,
1171    report: &ValidationReport,
1172    validation_profile: &str,
1173    gene: &Gene,
1174    blast_radius: &BlastRadius,
1175) -> Result<Capsule, EvolutionError> {
1176    let env = current_env_fingerprint(&receipt.workdir);
1177    let validator_hash = stable_hash_json(report)?;
1178    let diff_hash = mutation.artifact.content_hash.clone();
1179    let id = stable_hash_json(&(run_id, &gene.id, &diff_hash, &mutation.intent.id))?;
1180    Ok(Capsule {
1181        id,
1182        gene_id: gene.id.clone(),
1183        mutation_id: mutation.intent.id.clone(),
1184        run_id: run_id.clone(),
1185        diff_hash,
1186        confidence: 0.7,
1187        env,
1188        outcome: oris_evolution::Outcome {
1189            success: true,
1190            validation_profile: validation_profile.to_string(),
1191            validation_duration_ms: report.duration_ms,
1192            changed_files: receipt
1193                .changed_files
1194                .iter()
1195                .map(|path| path.to_string_lossy().to_string())
1196                .collect(),
1197            validator_hash,
1198            lines_changed: blast_radius.lines_changed,
1199            replay_verified: false,
1200        },
1201        state: AssetState::Promoted,
1202    })
1203}
1204
1205fn current_env_fingerprint(workdir: &Path) -> EnvFingerprint {
1206    let rustc_version = Command::new("rustc")
1207        .arg("--version")
1208        .output()
1209        .ok()
1210        .filter(|output| output.status.success())
1211        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
1212        .unwrap_or_else(|| "rustc unknown".into());
1213    let cargo_lock_hash = fs::read(workdir.join("Cargo.lock"))
1214        .ok()
1215        .map(|bytes| {
1216            let value = String::from_utf8_lossy(&bytes);
1217            compute_artifact_hash(&value)
1218        })
1219        .unwrap_or_else(|| "missing-cargo-lock".into());
1220    let target_triple = format!(
1221        "{}-unknown-{}",
1222        std::env::consts::ARCH,
1223        std::env::consts::OS
1224    );
1225    EnvFingerprint {
1226        rustc_version,
1227        cargo_lock_hash,
1228        target_triple,
1229        os: std::env::consts::OS.to_string(),
1230    }
1231}
1232
1233fn find_declared_mutation(
1234    store: &dyn EvolutionStore,
1235    mutation_id: &MutationId,
1236) -> Result<Option<PreparedMutation>, EvolutionError> {
1237    for stored in store.scan(1)? {
1238        if let EvolutionEvent::MutationDeclared { mutation } = stored.event {
1239            if &mutation.intent.id == mutation_id {
1240                return Ok(Some(mutation));
1241            }
1242        }
1243    }
1244    Ok(None)
1245}
1246
1247fn exact_match_candidates(store: &dyn EvolutionStore, input: &SelectorInput) -> Vec<GeneCandidate> {
1248    let Ok(projection) = store.rebuild_projection() else {
1249        return Vec::new();
1250    };
1251    let capsules = projection.capsules.clone();
1252    let spec_ids_by_gene = projection.spec_ids_by_gene.clone();
1253    let requested_spec_id = input
1254        .spec_id
1255        .as_deref()
1256        .map(str::trim)
1257        .filter(|value| !value.is_empty());
1258    let signal_set = input
1259        .signals
1260        .iter()
1261        .map(|signal| signal.to_ascii_lowercase())
1262        .collect::<BTreeSet<_>>();
1263    let mut candidates = projection
1264        .genes
1265        .into_iter()
1266        .filter_map(|gene| {
1267            if gene.state != AssetState::Promoted {
1268                return None;
1269            }
1270            if let Some(spec_id) = requested_spec_id {
1271                let matches_spec = spec_ids_by_gene
1272                    .get(&gene.id)
1273                    .map(|values| {
1274                        values
1275                            .iter()
1276                            .any(|value| value.eq_ignore_ascii_case(spec_id))
1277                    })
1278                    .unwrap_or(false);
1279                if !matches_spec {
1280                    return None;
1281                }
1282            }
1283            let gene_signals = gene
1284                .signals
1285                .iter()
1286                .map(|signal| signal.to_ascii_lowercase())
1287                .collect::<BTreeSet<_>>();
1288            if gene_signals == signal_set {
1289                let mut matched_capsules = capsules
1290                    .iter()
1291                    .filter(|capsule| {
1292                        capsule.gene_id == gene.id && capsule.state == AssetState::Promoted
1293                    })
1294                    .cloned()
1295                    .collect::<Vec<_>>();
1296                matched_capsules.sort_by(|left, right| {
1297                    replay_environment_match_factor(&input.env, &right.env)
1298                        .partial_cmp(&replay_environment_match_factor(&input.env, &left.env))
1299                        .unwrap_or(std::cmp::Ordering::Equal)
1300                        .then_with(|| {
1301                            right
1302                                .confidence
1303                                .partial_cmp(&left.confidence)
1304                                .unwrap_or(std::cmp::Ordering::Equal)
1305                        })
1306                        .then_with(|| left.id.cmp(&right.id))
1307                });
1308                if matched_capsules.is_empty() {
1309                    None
1310                } else {
1311                    let score = matched_capsules
1312                        .first()
1313                        .map(|capsule| replay_environment_match_factor(&input.env, &capsule.env))
1314                        .unwrap_or(0.0);
1315                    Some(GeneCandidate {
1316                        gene,
1317                        score,
1318                        capsules: matched_capsules,
1319                    })
1320                }
1321            } else {
1322                None
1323            }
1324        })
1325        .collect::<Vec<_>>();
1326    candidates.sort_by(|left, right| {
1327        right
1328            .score
1329            .partial_cmp(&left.score)
1330            .unwrap_or(std::cmp::Ordering::Equal)
1331            .then_with(|| left.gene.id.cmp(&right.gene.id))
1332    });
1333    candidates
1334}
1335
1336fn quarantined_remote_exact_match_candidates(
1337    store: &dyn EvolutionStore,
1338    input: &SelectorInput,
1339) -> Vec<GeneCandidate> {
1340    let remote_asset_ids = store
1341        .scan(1)
1342        .ok()
1343        .map(|events| {
1344            events
1345                .into_iter()
1346                .filter_map(|stored| match stored.event {
1347                    EvolutionEvent::RemoteAssetImported {
1348                        source: CandidateSource::Remote,
1349                        asset_ids,
1350                    } => Some(asset_ids),
1351                    _ => None,
1352                })
1353                .flatten()
1354                .collect::<BTreeSet<_>>()
1355        })
1356        .unwrap_or_default();
1357    if remote_asset_ids.is_empty() {
1358        return Vec::new();
1359    }
1360
1361    let Ok(projection) = store.rebuild_projection() else {
1362        return Vec::new();
1363    };
1364    let capsules = projection.capsules.clone();
1365    let spec_ids_by_gene = projection.spec_ids_by_gene.clone();
1366    let requested_spec_id = input
1367        .spec_id
1368        .as_deref()
1369        .map(str::trim)
1370        .filter(|value| !value.is_empty());
1371    let signal_set = input
1372        .signals
1373        .iter()
1374        .map(|signal| signal.to_ascii_lowercase())
1375        .collect::<BTreeSet<_>>();
1376    let mut candidates = projection
1377        .genes
1378        .into_iter()
1379        .filter_map(|gene| {
1380            if gene.state != AssetState::Promoted {
1381                return None;
1382            }
1383            if let Some(spec_id) = requested_spec_id {
1384                let matches_spec = spec_ids_by_gene
1385                    .get(&gene.id)
1386                    .map(|values| {
1387                        values
1388                            .iter()
1389                            .any(|value| value.eq_ignore_ascii_case(spec_id))
1390                    })
1391                    .unwrap_or(false);
1392                if !matches_spec {
1393                    return None;
1394                }
1395            }
1396            let gene_signals = gene
1397                .signals
1398                .iter()
1399                .map(|signal| signal.to_ascii_lowercase())
1400                .collect::<BTreeSet<_>>();
1401            if gene_signals == signal_set {
1402                let mut matched_capsules = capsules
1403                    .iter()
1404                    .filter(|capsule| {
1405                        capsule.gene_id == gene.id
1406                            && capsule.state == AssetState::Quarantined
1407                            && remote_asset_ids.contains(&capsule.id)
1408                    })
1409                    .cloned()
1410                    .collect::<Vec<_>>();
1411                matched_capsules.sort_by(|left, right| {
1412                    replay_environment_match_factor(&input.env, &right.env)
1413                        .partial_cmp(&replay_environment_match_factor(&input.env, &left.env))
1414                        .unwrap_or(std::cmp::Ordering::Equal)
1415                        .then_with(|| {
1416                            right
1417                                .confidence
1418                                .partial_cmp(&left.confidence)
1419                                .unwrap_or(std::cmp::Ordering::Equal)
1420                        })
1421                        .then_with(|| left.id.cmp(&right.id))
1422                });
1423                if matched_capsules.is_empty() {
1424                    None
1425                } else {
1426                    let score = matched_capsules
1427                        .first()
1428                        .map(|capsule| replay_environment_match_factor(&input.env, &capsule.env))
1429                        .unwrap_or(0.0);
1430                    Some(GeneCandidate {
1431                        gene,
1432                        score,
1433                        capsules: matched_capsules,
1434                    })
1435                }
1436            } else {
1437                None
1438            }
1439        })
1440        .collect::<Vec<_>>();
1441    candidates.sort_by(|left, right| {
1442        right
1443            .score
1444            .partial_cmp(&left.score)
1445            .unwrap_or(std::cmp::Ordering::Equal)
1446            .then_with(|| left.gene.id.cmp(&right.gene.id))
1447    });
1448    candidates
1449}
1450
1451fn replay_environment_match_factor(input: &EnvFingerprint, candidate: &EnvFingerprint) -> f32 {
1452    let fields = [
1453        input
1454            .rustc_version
1455            .eq_ignore_ascii_case(&candidate.rustc_version),
1456        input
1457            .cargo_lock_hash
1458            .eq_ignore_ascii_case(&candidate.cargo_lock_hash),
1459        input
1460            .target_triple
1461            .eq_ignore_ascii_case(&candidate.target_triple),
1462        input.os.eq_ignore_ascii_case(&candidate.os),
1463    ];
1464    let matched_fields = fields.into_iter().filter(|matched| *matched).count() as f32;
1465    0.5 + ((matched_fields / 4.0) * 0.5)
1466}
1467
1468fn effective_candidate_score(
1469    candidate: &GeneCandidate,
1470    publishers_by_gene: &BTreeMap<String, String>,
1471    reputation_bias: &BTreeMap<String, f32>,
1472) -> f32 {
1473    let bias = publishers_by_gene
1474        .get(&candidate.gene.id)
1475        .and_then(|publisher| reputation_bias.get(publisher))
1476        .copied()
1477        .unwrap_or(0.0)
1478        .clamp(0.0, 1.0);
1479    candidate.score * (1.0 + (bias * 0.1))
1480}
1481
1482fn export_promoted_assets_from_store(
1483    store: &dyn EvolutionStore,
1484    sender_id: impl Into<String>,
1485) -> Result<EvolutionEnvelope, EvoKernelError> {
1486    let projection = store.rebuild_projection().map_err(store_err)?;
1487    let mut assets = Vec::new();
1488    for gene in projection
1489        .genes
1490        .into_iter()
1491        .filter(|gene| gene.state == AssetState::Promoted)
1492    {
1493        assets.push(NetworkAsset::Gene { gene });
1494    }
1495    for capsule in projection
1496        .capsules
1497        .into_iter()
1498        .filter(|capsule| capsule.state == AssetState::Promoted)
1499    {
1500        assets.push(NetworkAsset::Capsule { capsule });
1501    }
1502    Ok(EvolutionEnvelope::publish(sender_id, assets))
1503}
1504
1505fn import_remote_envelope_into_store(
1506    store: &dyn EvolutionStore,
1507    envelope: &EvolutionEnvelope,
1508) -> Result<ImportOutcome, EvoKernelError> {
1509    if !envelope.verify_content_hash() {
1510        return Err(EvoKernelError::Validation(
1511            "invalid evolution envelope hash".into(),
1512        ));
1513    }
1514
1515    let mut imported_asset_ids = Vec::new();
1516    for asset in &envelope.assets {
1517        match asset {
1518            NetworkAsset::Gene { gene } => {
1519                imported_asset_ids.push(gene.id.clone());
1520                store
1521                    .append_event(EvolutionEvent::RemoteAssetImported {
1522                        source: CandidateSource::Remote,
1523                        asset_ids: vec![gene.id.clone()],
1524                    })
1525                    .map_err(store_err)?;
1526                store
1527                    .append_event(EvolutionEvent::GeneProjected { gene: gene.clone() })
1528                    .map_err(store_err)?;
1529            }
1530            NetworkAsset::Capsule { capsule } => {
1531                imported_asset_ids.push(capsule.id.clone());
1532                store
1533                    .append_event(EvolutionEvent::RemoteAssetImported {
1534                        source: CandidateSource::Remote,
1535                        asset_ids: vec![capsule.id.clone()],
1536                    })
1537                    .map_err(store_err)?;
1538                let mut quarantined = capsule.clone();
1539                quarantined.state = AssetState::Quarantined;
1540                store
1541                    .append_event(EvolutionEvent::CapsuleCommitted {
1542                        capsule: quarantined.clone(),
1543                    })
1544                    .map_err(store_err)?;
1545                store
1546                    .append_event(EvolutionEvent::CapsuleQuarantined {
1547                        capsule_id: quarantined.id,
1548                    })
1549                    .map_err(store_err)?;
1550            }
1551            NetworkAsset::EvolutionEvent { event } => {
1552                if should_import_remote_event(event) {
1553                    store.append_event(event.clone()).map_err(store_err)?;
1554                }
1555            }
1556        }
1557    }
1558
1559    Ok(ImportOutcome {
1560        imported_asset_ids,
1561        accepted: true,
1562    })
1563}
1564
1565fn should_import_remote_event(event: &EvolutionEvent) -> bool {
1566    matches!(
1567        event,
1568        EvolutionEvent::MutationDeclared { .. } | EvolutionEvent::SpecLinked { .. }
1569    )
1570}
1571
1572fn fetch_assets_from_store(
1573    store: &dyn EvolutionStore,
1574    responder_id: impl Into<String>,
1575    query: &FetchQuery,
1576) -> Result<FetchResponse, EvoKernelError> {
1577    let projection = store.rebuild_projection().map_err(store_err)?;
1578    let normalized_signals: Vec<String> = query
1579        .signals
1580        .iter()
1581        .map(|signal| signal.trim().to_ascii_lowercase())
1582        .filter(|signal| !signal.is_empty())
1583        .collect();
1584    let matches_any_signal = |candidate: &str| {
1585        if normalized_signals.is_empty() {
1586            return true;
1587        }
1588        let candidate = candidate.to_ascii_lowercase();
1589        normalized_signals
1590            .iter()
1591            .any(|signal| candidate.contains(signal) || signal.contains(&candidate))
1592    };
1593
1594    let matched_genes: Vec<Gene> = projection
1595        .genes
1596        .into_iter()
1597        .filter(|gene| gene.state == AssetState::Promoted)
1598        .filter(|gene| gene.signals.iter().any(|signal| matches_any_signal(signal)))
1599        .collect();
1600    let matched_gene_ids: BTreeSet<String> =
1601        matched_genes.iter().map(|gene| gene.id.clone()).collect();
1602    let matched_capsules: Vec<Capsule> = projection
1603        .capsules
1604        .into_iter()
1605        .filter(|capsule| capsule.state == AssetState::Promoted)
1606        .filter(|capsule| matched_gene_ids.contains(&capsule.gene_id))
1607        .collect();
1608
1609    let mut assets = Vec::new();
1610    for gene in matched_genes {
1611        assets.push(NetworkAsset::Gene { gene });
1612    }
1613    for capsule in matched_capsules {
1614        assets.push(NetworkAsset::Capsule { capsule });
1615    }
1616
1617    Ok(FetchResponse {
1618        sender_id: responder_id.into(),
1619        assets,
1620    })
1621}
1622
1623fn revoke_assets_in_store(
1624    store: &dyn EvolutionStore,
1625    notice: &RevokeNotice,
1626) -> Result<RevokeNotice, EvoKernelError> {
1627    let projection = store.rebuild_projection().map_err(store_err)?;
1628    let requested: BTreeSet<String> = notice
1629        .asset_ids
1630        .iter()
1631        .map(|asset_id| asset_id.trim().to_string())
1632        .filter(|asset_id| !asset_id.is_empty())
1633        .collect();
1634    let mut revoked_gene_ids = BTreeSet::new();
1635    let mut quarantined_capsule_ids = BTreeSet::new();
1636
1637    for gene in &projection.genes {
1638        if requested.contains(&gene.id) {
1639            revoked_gene_ids.insert(gene.id.clone());
1640        }
1641    }
1642    for capsule in &projection.capsules {
1643        if requested.contains(&capsule.id) {
1644            quarantined_capsule_ids.insert(capsule.id.clone());
1645            revoked_gene_ids.insert(capsule.gene_id.clone());
1646        }
1647    }
1648    for capsule in &projection.capsules {
1649        if revoked_gene_ids.contains(&capsule.gene_id) {
1650            quarantined_capsule_ids.insert(capsule.id.clone());
1651        }
1652    }
1653
1654    for gene_id in &revoked_gene_ids {
1655        store
1656            .append_event(EvolutionEvent::GeneRevoked {
1657                gene_id: gene_id.clone(),
1658                reason: notice.reason.clone(),
1659            })
1660            .map_err(store_err)?;
1661    }
1662    for capsule_id in &quarantined_capsule_ids {
1663        store
1664            .append_event(EvolutionEvent::CapsuleQuarantined {
1665                capsule_id: capsule_id.clone(),
1666            })
1667            .map_err(store_err)?;
1668    }
1669
1670    let mut affected_ids: Vec<String> = revoked_gene_ids.into_iter().collect();
1671    affected_ids.extend(quarantined_capsule_ids);
1672    affected_ids.sort();
1673    affected_ids.dedup();
1674
1675    Ok(RevokeNotice {
1676        sender_id: notice.sender_id.clone(),
1677        asset_ids: affected_ids,
1678        reason: notice.reason.clone(),
1679    })
1680}
1681
1682fn evolution_metrics_snapshot(
1683    store: &dyn EvolutionStore,
1684) -> Result<EvolutionMetricsSnapshot, EvoKernelError> {
1685    let events = store.scan(1).map_err(store_err)?;
1686    let projection = store.rebuild_projection().map_err(store_err)?;
1687    let replay_success_total = events
1688        .iter()
1689        .filter(|stored| matches!(stored.event, EvolutionEvent::CapsuleReused { .. }))
1690        .count() as u64;
1691    let replay_failures_total = events
1692        .iter()
1693        .filter(|stored| is_replay_validation_failure(&stored.event))
1694        .count() as u64;
1695    let replay_attempts_total = replay_success_total + replay_failures_total;
1696    let mutation_declared_total = events
1697        .iter()
1698        .filter(|stored| matches!(stored.event, EvolutionEvent::MutationDeclared { .. }))
1699        .count() as u64;
1700    let promoted_mutations_total = events
1701        .iter()
1702        .filter(|stored| matches!(stored.event, EvolutionEvent::GenePromoted { .. }))
1703        .count() as u64;
1704    let gene_revocations_total = events
1705        .iter()
1706        .filter(|stored| matches!(stored.event, EvolutionEvent::GeneRevoked { .. }))
1707        .count() as u64;
1708    let cutoff = Utc::now() - Duration::hours(1);
1709    let mutation_velocity_last_hour = count_recent_events(&events, cutoff, |event| {
1710        matches!(event, EvolutionEvent::MutationDeclared { .. })
1711    });
1712    let revoke_frequency_last_hour = count_recent_events(&events, cutoff, |event| {
1713        matches!(event, EvolutionEvent::GeneRevoked { .. })
1714    });
1715    let promoted_genes = projection
1716        .genes
1717        .iter()
1718        .filter(|gene| gene.state == AssetState::Promoted)
1719        .count() as u64;
1720    let promoted_capsules = projection
1721        .capsules
1722        .iter()
1723        .filter(|capsule| capsule.state == AssetState::Promoted)
1724        .count() as u64;
1725
1726    Ok(EvolutionMetricsSnapshot {
1727        replay_attempts_total,
1728        replay_success_total,
1729        replay_success_rate: safe_ratio(replay_success_total, replay_attempts_total),
1730        mutation_declared_total,
1731        promoted_mutations_total,
1732        promotion_ratio: safe_ratio(promoted_mutations_total, mutation_declared_total),
1733        gene_revocations_total,
1734        mutation_velocity_last_hour,
1735        revoke_frequency_last_hour,
1736        promoted_genes,
1737        promoted_capsules,
1738        last_event_seq: events.last().map(|stored| stored.seq).unwrap_or(0),
1739    })
1740}
1741
1742fn evolution_health_snapshot(snapshot: &EvolutionMetricsSnapshot) -> EvolutionHealthSnapshot {
1743    EvolutionHealthSnapshot {
1744        status: "ok".into(),
1745        last_event_seq: snapshot.last_event_seq,
1746        promoted_genes: snapshot.promoted_genes,
1747        promoted_capsules: snapshot.promoted_capsules,
1748    }
1749}
1750
1751fn render_evolution_metrics_prometheus(
1752    snapshot: &EvolutionMetricsSnapshot,
1753    health: &EvolutionHealthSnapshot,
1754) -> String {
1755    let mut out = String::new();
1756    out.push_str(
1757        "# HELP oris_evolution_replay_attempts_total Total replay attempts that reached validation.\n",
1758    );
1759    out.push_str("# TYPE oris_evolution_replay_attempts_total counter\n");
1760    out.push_str(&format!(
1761        "oris_evolution_replay_attempts_total {}\n",
1762        snapshot.replay_attempts_total
1763    ));
1764    out.push_str("# HELP oris_evolution_replay_success_total Total replay attempts that reused a capsule successfully.\n");
1765    out.push_str("# TYPE oris_evolution_replay_success_total counter\n");
1766    out.push_str(&format!(
1767        "oris_evolution_replay_success_total {}\n",
1768        snapshot.replay_success_total
1769    ));
1770    out.push_str("# HELP oris_evolution_replay_success_rate Successful replay attempts divided by replay attempts that reached validation.\n");
1771    out.push_str("# TYPE oris_evolution_replay_success_rate gauge\n");
1772    out.push_str(&format!(
1773        "oris_evolution_replay_success_rate {:.6}\n",
1774        snapshot.replay_success_rate
1775    ));
1776    out.push_str(
1777        "# HELP oris_evolution_mutation_declared_total Total declared mutations recorded in the evolution log.\n",
1778    );
1779    out.push_str("# TYPE oris_evolution_mutation_declared_total counter\n");
1780    out.push_str(&format!(
1781        "oris_evolution_mutation_declared_total {}\n",
1782        snapshot.mutation_declared_total
1783    ));
1784    out.push_str("# HELP oris_evolution_promoted_mutations_total Total mutations promoted by the governor.\n");
1785    out.push_str("# TYPE oris_evolution_promoted_mutations_total counter\n");
1786    out.push_str(&format!(
1787        "oris_evolution_promoted_mutations_total {}\n",
1788        snapshot.promoted_mutations_total
1789    ));
1790    out.push_str(
1791        "# HELP oris_evolution_promotion_ratio Promoted mutations divided by declared mutations.\n",
1792    );
1793    out.push_str("# TYPE oris_evolution_promotion_ratio gauge\n");
1794    out.push_str(&format!(
1795        "oris_evolution_promotion_ratio {:.6}\n",
1796        snapshot.promotion_ratio
1797    ));
1798    out.push_str("# HELP oris_evolution_gene_revocations_total Total gene revocations recorded in the evolution log.\n");
1799    out.push_str("# TYPE oris_evolution_gene_revocations_total counter\n");
1800    out.push_str(&format!(
1801        "oris_evolution_gene_revocations_total {}\n",
1802        snapshot.gene_revocations_total
1803    ));
1804    out.push_str("# HELP oris_evolution_mutation_velocity_last_hour Declared mutations observed in the last hour.\n");
1805    out.push_str("# TYPE oris_evolution_mutation_velocity_last_hour gauge\n");
1806    out.push_str(&format!(
1807        "oris_evolution_mutation_velocity_last_hour {}\n",
1808        snapshot.mutation_velocity_last_hour
1809    ));
1810    out.push_str("# HELP oris_evolution_revoke_frequency_last_hour Gene revocations observed in the last hour.\n");
1811    out.push_str("# TYPE oris_evolution_revoke_frequency_last_hour gauge\n");
1812    out.push_str(&format!(
1813        "oris_evolution_revoke_frequency_last_hour {}\n",
1814        snapshot.revoke_frequency_last_hour
1815    ));
1816    out.push_str("# HELP oris_evolution_promoted_genes Current promoted genes in the evolution projection.\n");
1817    out.push_str("# TYPE oris_evolution_promoted_genes gauge\n");
1818    out.push_str(&format!(
1819        "oris_evolution_promoted_genes {}\n",
1820        snapshot.promoted_genes
1821    ));
1822    out.push_str("# HELP oris_evolution_promoted_capsules Current promoted capsules in the evolution projection.\n");
1823    out.push_str("# TYPE oris_evolution_promoted_capsules gauge\n");
1824    out.push_str(&format!(
1825        "oris_evolution_promoted_capsules {}\n",
1826        snapshot.promoted_capsules
1827    ));
1828    out.push_str("# HELP oris_evolution_store_last_event_seq Last visible append-only evolution event sequence.\n");
1829    out.push_str("# TYPE oris_evolution_store_last_event_seq gauge\n");
1830    out.push_str(&format!(
1831        "oris_evolution_store_last_event_seq {}\n",
1832        snapshot.last_event_seq
1833    ));
1834    out.push_str(
1835        "# HELP oris_evolution_health Evolution observability store health (1 = healthy).\n",
1836    );
1837    out.push_str("# TYPE oris_evolution_health gauge\n");
1838    out.push_str(&format!(
1839        "oris_evolution_health {}\n",
1840        u8::from(health.status == "ok")
1841    ));
1842    out
1843}
1844
1845fn count_recent_events(
1846    events: &[StoredEvolutionEvent],
1847    cutoff: DateTime<Utc>,
1848    predicate: impl Fn(&EvolutionEvent) -> bool,
1849) -> u64 {
1850    events
1851        .iter()
1852        .filter(|stored| {
1853            predicate(&stored.event)
1854                && parse_event_timestamp(&stored.timestamp)
1855                    .map(|timestamp| timestamp >= cutoff)
1856                    .unwrap_or(false)
1857        })
1858        .count() as u64
1859}
1860
1861fn parse_event_timestamp(raw: &str) -> Option<DateTime<Utc>> {
1862    DateTime::parse_from_rfc3339(raw)
1863        .ok()
1864        .map(|parsed| parsed.with_timezone(&Utc))
1865}
1866
1867fn is_replay_validation_failure(event: &EvolutionEvent) -> bool {
1868    matches!(
1869        event,
1870        EvolutionEvent::ValidationFailed {
1871            gene_id: Some(_),
1872            ..
1873        }
1874    )
1875}
1876
1877fn safe_ratio(numerator: u64, denominator: u64) -> f64 {
1878    if denominator == 0 {
1879        0.0
1880    } else {
1881        numerator as f64 / denominator as f64
1882    }
1883}
1884
1885fn store_err(err: EvolutionError) -> EvoKernelError {
1886    EvoKernelError::Store(err.to_string())
1887}
1888
1889#[cfg(test)]
1890mod tests {
1891    use super::*;
1892    use oris_kernel::{
1893        AllowAllPolicy, InMemoryEventStore, KernelMode, KernelState, NoopActionExecutor,
1894        NoopStepFn, StateUpdatedOnlyReducer,
1895    };
1896    use serde::{Deserialize, Serialize};
1897
1898    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1899    struct TestState;
1900
1901    impl KernelState for TestState {
1902        fn version(&self) -> u32 {
1903            1
1904        }
1905    }
1906
1907    fn temp_workspace(name: &str) -> std::path::PathBuf {
1908        let root =
1909            std::env::temp_dir().join(format!("oris-evokernel-{name}-{}", std::process::id()));
1910        if root.exists() {
1911            fs::remove_dir_all(&root).unwrap();
1912        }
1913        fs::create_dir_all(root.join("src")).unwrap();
1914        fs::write(
1915            root.join("Cargo.toml"),
1916            "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
1917        )
1918        .unwrap();
1919        fs::write(root.join("Cargo.lock"), "# lock\n").unwrap();
1920        fs::write(root.join("src/lib.rs"), "pub fn demo() -> usize { 1 }\n").unwrap();
1921        root
1922    }
1923
1924    fn test_kernel() -> Arc<Kernel<TestState>> {
1925        Arc::new(Kernel::<TestState> {
1926            events: Box::new(InMemoryEventStore::new()),
1927            snaps: None,
1928            reducer: Box::new(StateUpdatedOnlyReducer),
1929            exec: Box::new(NoopActionExecutor),
1930            step: Box::new(NoopStepFn),
1931            policy: Box::new(AllowAllPolicy),
1932            effect_sink: None,
1933            mode: KernelMode::Normal,
1934        })
1935    }
1936
1937    fn lightweight_plan() -> ValidationPlan {
1938        ValidationPlan {
1939            profile: "test".into(),
1940            stages: vec![ValidationStage::Command {
1941                program: "git".into(),
1942                args: vec!["--version".into()],
1943                timeout_ms: 5_000,
1944            }],
1945        }
1946    }
1947
1948    fn sample_mutation() -> PreparedMutation {
1949        prepare_mutation(
1950            MutationIntent {
1951                id: "mutation-1".into(),
1952                intent: "add README".into(),
1953                target: MutationTarget::Paths {
1954                    allow: vec!["README.md".into()],
1955                },
1956                expected_effect: "repo still builds".into(),
1957                risk: RiskLevel::Low,
1958                signals: vec!["missing readme".into()],
1959                spec_id: None,
1960            },
1961            "\
1962diff --git a/README.md b/README.md
1963new file mode 100644
1964index 0000000..1111111
1965--- /dev/null
1966+++ b/README.md
1967@@ -0,0 +1 @@
1968+# sample
1969"
1970            .into(),
1971            Some("HEAD".into()),
1972        )
1973    }
1974
1975    fn base_sandbox_policy() -> SandboxPolicy {
1976        SandboxPolicy {
1977            allowed_programs: vec!["git".into()],
1978            max_duration_ms: 60_000,
1979            max_output_bytes: 1024 * 1024,
1980            denied_env_prefixes: Vec::new(),
1981        }
1982    }
1983
1984    fn command_validator() -> Arc<dyn Validator> {
1985        Arc::new(CommandValidator::new(base_sandbox_policy()))
1986    }
1987
1988    fn replay_input(signal: &str) -> SelectorInput {
1989        SelectorInput {
1990            signals: vec![signal.into()],
1991            env: EnvFingerprint {
1992                rustc_version: "rustc".into(),
1993                cargo_lock_hash: "lock".into(),
1994                target_triple: "x86_64-unknown-linux-gnu".into(),
1995                os: std::env::consts::OS.into(),
1996            },
1997            spec_id: None,
1998            limit: 1,
1999        }
2000    }
2001
2002    fn build_test_evo_with_store(
2003        name: &str,
2004        run_id: &str,
2005        validator: Arc<dyn Validator>,
2006        store: Arc<dyn EvolutionStore>,
2007    ) -> EvoKernel<TestState> {
2008        let workspace = temp_workspace(name);
2009        let sandbox: Arc<dyn Sandbox> = Arc::new(oris_sandbox::LocalProcessSandbox::new(
2010            run_id,
2011            &workspace,
2012            std::env::temp_dir(),
2013        ));
2014        EvoKernel::new(test_kernel(), sandbox, validator, store)
2015            .with_governor(Arc::new(DefaultGovernor::new(
2016                oris_governor::GovernorConfig {
2017                    promote_after_successes: 1,
2018                    ..Default::default()
2019                },
2020            )))
2021            .with_validation_plan(lightweight_plan())
2022            .with_sandbox_policy(base_sandbox_policy())
2023    }
2024
2025    fn build_test_evo(
2026        name: &str,
2027        run_id: &str,
2028        validator: Arc<dyn Validator>,
2029    ) -> (EvoKernel<TestState>, Arc<dyn EvolutionStore>) {
2030        let store_root = std::env::temp_dir().join(format!(
2031            "oris-evokernel-{name}-store-{}",
2032            std::process::id()
2033        ));
2034        if store_root.exists() {
2035            fs::remove_dir_all(&store_root).unwrap();
2036        }
2037        let store: Arc<dyn EvolutionStore> =
2038            Arc::new(oris_evolution::JsonlEvolutionStore::new(&store_root));
2039        let evo = build_test_evo_with_store(name, run_id, validator, store.clone());
2040        (evo, store)
2041    }
2042
2043    fn remote_publish_envelope(
2044        sender_id: &str,
2045        run_id: &str,
2046        gene_id: &str,
2047        capsule_id: &str,
2048        mutation_id: &str,
2049        signal: &str,
2050        file_name: &str,
2051        line: &str,
2052    ) -> EvolutionEnvelope {
2053        remote_publish_envelope_with_env(
2054            sender_id,
2055            run_id,
2056            gene_id,
2057            capsule_id,
2058            mutation_id,
2059            signal,
2060            file_name,
2061            line,
2062            replay_input(signal).env,
2063        )
2064    }
2065
2066    fn remote_publish_envelope_with_env(
2067        sender_id: &str,
2068        run_id: &str,
2069        gene_id: &str,
2070        capsule_id: &str,
2071        mutation_id: &str,
2072        signal: &str,
2073        file_name: &str,
2074        line: &str,
2075        env: EnvFingerprint,
2076    ) -> EvolutionEnvelope {
2077        let mutation = prepare_mutation(
2078            MutationIntent {
2079                id: mutation_id.into(),
2080                intent: format!("add {file_name}"),
2081                target: MutationTarget::Paths {
2082                    allow: vec![file_name.into()],
2083                },
2084                expected_effect: "replay should still validate".into(),
2085                risk: RiskLevel::Low,
2086                signals: vec![signal.into()],
2087                spec_id: None,
2088            },
2089            format!(
2090                "\
2091diff --git a/{file_name} b/{file_name}
2092new file mode 100644
2093index 0000000..1111111
2094--- /dev/null
2095+++ b/{file_name}
2096@@ -0,0 +1 @@
2097+{line}
2098"
2099            ),
2100            Some("HEAD".into()),
2101        );
2102        let gene = Gene {
2103            id: gene_id.into(),
2104            signals: vec![signal.into()],
2105            strategy: vec![file_name.into()],
2106            validation: vec!["test".into()],
2107            state: AssetState::Promoted,
2108        };
2109        let capsule = Capsule {
2110            id: capsule_id.into(),
2111            gene_id: gene_id.into(),
2112            mutation_id: mutation_id.into(),
2113            run_id: run_id.into(),
2114            diff_hash: mutation.artifact.content_hash.clone(),
2115            confidence: 0.9,
2116            env,
2117            outcome: Outcome {
2118                success: true,
2119                validation_profile: "test".into(),
2120                validation_duration_ms: 1,
2121                changed_files: vec![file_name.into()],
2122                validator_hash: "validator-hash".into(),
2123                lines_changed: 1,
2124                replay_verified: false,
2125            },
2126            state: AssetState::Promoted,
2127        };
2128        EvolutionEnvelope::publish(
2129            sender_id,
2130            vec![
2131                NetworkAsset::EvolutionEvent {
2132                    event: EvolutionEvent::MutationDeclared { mutation },
2133                },
2134                NetworkAsset::Gene { gene: gene.clone() },
2135                NetworkAsset::Capsule {
2136                    capsule: capsule.clone(),
2137                },
2138                NetworkAsset::EvolutionEvent {
2139                    event: EvolutionEvent::CapsuleReleased {
2140                        capsule_id: capsule.id.clone(),
2141                        state: AssetState::Promoted,
2142                    },
2143                },
2144            ],
2145        )
2146    }
2147
2148    struct FixedValidator {
2149        success: bool,
2150    }
2151
2152    #[async_trait]
2153    impl Validator for FixedValidator {
2154        async fn run(
2155            &self,
2156            _receipt: &SandboxReceipt,
2157            plan: &ValidationPlan,
2158        ) -> Result<ValidationReport, ValidationError> {
2159            Ok(ValidationReport {
2160                success: self.success,
2161                duration_ms: 1,
2162                stages: Vec::new(),
2163                logs: if self.success {
2164                    format!("{} ok", plan.profile)
2165                } else {
2166                    format!("{} failed", plan.profile)
2167                },
2168            })
2169        }
2170    }
2171
2172    #[tokio::test]
2173    async fn command_validator_aggregates_stage_reports() {
2174        let workspace = temp_workspace("validator");
2175        let receipt = SandboxReceipt {
2176            mutation_id: "m".into(),
2177            workdir: workspace,
2178            applied: true,
2179            changed_files: Vec::new(),
2180            patch_hash: "hash".into(),
2181            stdout_log: std::env::temp_dir().join("stdout.log"),
2182            stderr_log: std::env::temp_dir().join("stderr.log"),
2183        };
2184        let validator = CommandValidator::new(SandboxPolicy {
2185            allowed_programs: vec!["git".into()],
2186            max_duration_ms: 1_000,
2187            max_output_bytes: 1024,
2188            denied_env_prefixes: Vec::new(),
2189        });
2190        let report = validator
2191            .run(
2192                &receipt,
2193                &ValidationPlan {
2194                    profile: "test".into(),
2195                    stages: vec![ValidationStage::Command {
2196                        program: "git".into(),
2197                        args: vec!["--version".into()],
2198                        timeout_ms: 1_000,
2199                    }],
2200                },
2201            )
2202            .await
2203            .unwrap();
2204        assert_eq!(report.stages.len(), 1);
2205    }
2206
2207    #[tokio::test]
2208    async fn capture_successful_mutation_appends_capsule() {
2209        let (evo, store) = build_test_evo("capture", "run-1", command_validator());
2210        let capsule = evo
2211            .capture_successful_mutation(&"run-1".into(), sample_mutation())
2212            .await
2213            .unwrap();
2214        let events = store.scan(1).unwrap();
2215        assert!(events
2216            .iter()
2217            .any(|stored| matches!(stored.event, EvolutionEvent::CapsuleCommitted { .. })));
2218        assert!(!capsule.id.is_empty());
2219    }
2220
2221    #[tokio::test]
2222    async fn replay_hit_records_capsule_reused() {
2223        let (evo, store) = build_test_evo("replay", "run-2", command_validator());
2224        let capsule = evo
2225            .capture_successful_mutation(&"run-2".into(), sample_mutation())
2226            .await
2227            .unwrap();
2228        let decision = evo
2229            .replay_or_fallback(replay_input("missing readme"))
2230            .await
2231            .unwrap();
2232        assert!(decision.used_capsule);
2233        assert_eq!(decision.capsule_id, Some(capsule.id));
2234        assert!(store
2235            .scan(1)
2236            .unwrap()
2237            .iter()
2238            .any(|stored| matches!(stored.event, EvolutionEvent::CapsuleReused { .. })));
2239    }
2240
2241    #[tokio::test]
2242    async fn metrics_snapshot_tracks_replay_promotion_and_revocation_signals() {
2243        let (evo, _) = build_test_evo("metrics", "run-metrics", command_validator());
2244        let capsule = evo
2245            .capture_successful_mutation(&"run-metrics".into(), sample_mutation())
2246            .await
2247            .unwrap();
2248        let decision = evo
2249            .replay_or_fallback(replay_input("missing readme"))
2250            .await
2251            .unwrap();
2252        assert!(decision.used_capsule);
2253
2254        evo.revoke_assets(&RevokeNotice {
2255            sender_id: "node-metrics".into(),
2256            asset_ids: vec![capsule.id.clone()],
2257            reason: "manual test revoke".into(),
2258        })
2259        .unwrap();
2260
2261        let snapshot = evo.metrics_snapshot().unwrap();
2262        assert_eq!(snapshot.replay_attempts_total, 1);
2263        assert_eq!(snapshot.replay_success_total, 1);
2264        assert_eq!(snapshot.replay_success_rate, 1.0);
2265        assert_eq!(snapshot.mutation_declared_total, 1);
2266        assert_eq!(snapshot.promoted_mutations_total, 1);
2267        assert_eq!(snapshot.promotion_ratio, 1.0);
2268        assert_eq!(snapshot.gene_revocations_total, 1);
2269        assert_eq!(snapshot.mutation_velocity_last_hour, 1);
2270        assert_eq!(snapshot.revoke_frequency_last_hour, 1);
2271        assert_eq!(snapshot.promoted_genes, 0);
2272        assert_eq!(snapshot.promoted_capsules, 0);
2273
2274        let rendered = evo.render_metrics_prometheus().unwrap();
2275        assert!(rendered.contains("oris_evolution_replay_success_rate 1.000000"));
2276        assert!(rendered.contains("oris_evolution_promotion_ratio 1.000000"));
2277        assert!(rendered.contains("oris_evolution_revoke_frequency_last_hour 1"));
2278        assert!(rendered.contains("oris_evolution_mutation_velocity_last_hour 1"));
2279        assert!(rendered.contains("oris_evolution_health 1"));
2280    }
2281
2282    #[tokio::test]
2283    async fn remote_replay_prefers_closest_environment_match() {
2284        let (evo, _) = build_test_evo("remote-env", "run-remote-env", command_validator());
2285        let input = replay_input("env-signal");
2286
2287        let envelope_a = remote_publish_envelope_with_env(
2288            "node-a",
2289            "run-remote-a",
2290            "gene-a",
2291            "capsule-a",
2292            "mutation-a",
2293            "env-signal",
2294            "A.md",
2295            "# from a",
2296            input.env.clone(),
2297        );
2298        let envelope_b = remote_publish_envelope_with_env(
2299            "node-b",
2300            "run-remote-b",
2301            "gene-b",
2302            "capsule-b",
2303            "mutation-b",
2304            "env-signal",
2305            "B.md",
2306            "# from b",
2307            EnvFingerprint {
2308                rustc_version: "old-rustc".into(),
2309                cargo_lock_hash: "other-lock".into(),
2310                target_triple: "aarch64-apple-darwin".into(),
2311                os: "linux".into(),
2312            },
2313        );
2314
2315        evo.import_remote_envelope(&envelope_a).unwrap();
2316        evo.import_remote_envelope(&envelope_b).unwrap();
2317
2318        let decision = evo.replay_or_fallback(input).await.unwrap();
2319
2320        assert!(decision.used_capsule);
2321        assert_eq!(decision.capsule_id, Some("capsule-a".into()));
2322        assert!(!decision.fallback_to_planner);
2323    }
2324
2325    #[tokio::test]
2326    async fn remote_capsule_stays_quarantined_until_first_successful_replay() {
2327        let (evo, store) = build_test_evo(
2328            "remote-quarantine",
2329            "run-remote-quarantine",
2330            command_validator(),
2331        );
2332        let envelope = remote_publish_envelope(
2333            "node-remote",
2334            "run-remote-quarantine",
2335            "gene-remote",
2336            "capsule-remote",
2337            "mutation-remote",
2338            "remote-signal",
2339            "REMOTE.md",
2340            "# from remote",
2341        );
2342
2343        evo.import_remote_envelope(&envelope).unwrap();
2344
2345        let before_replay = store.rebuild_projection().unwrap();
2346        let imported_capsule = before_replay
2347            .capsules
2348            .iter()
2349            .find(|capsule| capsule.id == "capsule-remote")
2350            .unwrap();
2351        assert_eq!(imported_capsule.state, AssetState::Quarantined);
2352
2353        let decision = evo
2354            .replay_or_fallback(replay_input("remote-signal"))
2355            .await
2356            .unwrap();
2357
2358        assert!(decision.used_capsule);
2359        assert_eq!(decision.capsule_id, Some("capsule-remote".into()));
2360
2361        let after_replay = store.rebuild_projection().unwrap();
2362        let released_capsule = after_replay
2363            .capsules
2364            .iter()
2365            .find(|capsule| capsule.id == "capsule-remote")
2366            .unwrap();
2367        assert_eq!(released_capsule.state, AssetState::Promoted);
2368    }
2369
2370    #[tokio::test]
2371    async fn insufficient_evu_blocks_publish_but_not_local_replay() {
2372        let (evo, _) = build_test_evo("stake-gate", "run-stake", command_validator());
2373        let capsule = evo
2374            .capture_successful_mutation(&"run-stake".into(), sample_mutation())
2375            .await
2376            .unwrap();
2377        let publish = evo.export_promoted_assets("node-local");
2378        assert!(matches!(publish, Err(EvoKernelError::Validation(_))));
2379
2380        let decision = evo
2381            .replay_or_fallback(replay_input("missing readme"))
2382            .await
2383            .unwrap();
2384        assert!(decision.used_capsule);
2385        assert_eq!(decision.capsule_id, Some(capsule.id));
2386    }
2387
2388    #[tokio::test]
2389    async fn second_replay_validation_failure_revokes_gene_immediately() {
2390        let (capturer, store) = build_test_evo("revoke-replay", "run-capture", command_validator());
2391        let capsule = capturer
2392            .capture_successful_mutation(&"run-capture".into(), sample_mutation())
2393            .await
2394            .unwrap();
2395
2396        let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
2397        let failing_replay = build_test_evo_with_store(
2398            "revoke-replay",
2399            "run-replay-fail",
2400            failing_validator,
2401            store.clone(),
2402        );
2403
2404        let first = failing_replay
2405            .replay_or_fallback(replay_input("missing readme"))
2406            .await
2407            .unwrap();
2408        let second = failing_replay
2409            .replay_or_fallback(replay_input("missing readme"))
2410            .await
2411            .unwrap();
2412
2413        assert!(!first.used_capsule);
2414        assert!(first.fallback_to_planner);
2415        assert!(!second.used_capsule);
2416        assert!(second.fallback_to_planner);
2417
2418        let projection = store.rebuild_projection().unwrap();
2419        let gene = projection
2420            .genes
2421            .iter()
2422            .find(|gene| gene.id == capsule.gene_id)
2423            .unwrap();
2424        assert_eq!(gene.state, AssetState::Revoked);
2425        let committed_capsule = projection
2426            .capsules
2427            .iter()
2428            .find(|current| current.id == capsule.id)
2429            .unwrap();
2430        assert_eq!(committed_capsule.state, AssetState::Quarantined);
2431
2432        let events = store.scan(1).unwrap();
2433        assert_eq!(
2434            events
2435                .iter()
2436                .filter(|stored| {
2437                    matches!(
2438                        &stored.event,
2439                        EvolutionEvent::ValidationFailed {
2440                            gene_id: Some(gene_id),
2441                            ..
2442                        } if gene_id == &capsule.gene_id
2443                    )
2444                })
2445                .count(),
2446            2
2447        );
2448        assert!(events.iter().any(|stored| {
2449            matches!(
2450                &stored.event,
2451                EvolutionEvent::GeneRevoked { gene_id, .. } if gene_id == &capsule.gene_id
2452            )
2453        }));
2454
2455        let recovered = build_test_evo_with_store(
2456            "revoke-replay",
2457            "run-replay-check",
2458            command_validator(),
2459            store.clone(),
2460        );
2461        let after_revoke = recovered
2462            .replay_or_fallback(replay_input("missing readme"))
2463            .await
2464            .unwrap();
2465        assert!(!after_revoke.used_capsule);
2466        assert!(after_revoke.fallback_to_planner);
2467        assert_eq!(after_revoke.reason, "no matching gene");
2468    }
2469
2470    #[tokio::test]
2471    async fn remote_reuse_success_rewards_publisher_and_biases_selection() {
2472        let ledger = Arc::new(Mutex::new(EvuLedger {
2473            accounts: vec![],
2474            reputations: vec![
2475                oris_economics::ReputationRecord {
2476                    node_id: "node-a".into(),
2477                    publish_success_rate: 0.4,
2478                    validator_accuracy: 0.4,
2479                    reuse_impact: 0,
2480                },
2481                oris_economics::ReputationRecord {
2482                    node_id: "node-b".into(),
2483                    publish_success_rate: 0.95,
2484                    validator_accuracy: 0.95,
2485                    reuse_impact: 8,
2486                },
2487            ],
2488        }));
2489        let (evo, _) = build_test_evo("remote-success", "run-remote", command_validator());
2490        let evo = evo.with_economics(ledger.clone());
2491
2492        let envelope_a = remote_publish_envelope(
2493            "node-a",
2494            "run-remote-a",
2495            "gene-a",
2496            "capsule-a",
2497            "mutation-a",
2498            "shared-signal",
2499            "A.md",
2500            "# from a",
2501        );
2502        let envelope_b = remote_publish_envelope(
2503            "node-b",
2504            "run-remote-b",
2505            "gene-b",
2506            "capsule-b",
2507            "mutation-b",
2508            "shared-signal",
2509            "B.md",
2510            "# from b",
2511        );
2512
2513        evo.import_remote_envelope(&envelope_a).unwrap();
2514        evo.import_remote_envelope(&envelope_b).unwrap();
2515
2516        let decision = evo
2517            .replay_or_fallback(replay_input("shared-signal"))
2518            .await
2519            .unwrap();
2520
2521        assert!(decision.used_capsule);
2522        assert_eq!(decision.capsule_id, Some("capsule-b".into()));
2523        let locked = ledger.lock().unwrap();
2524        let rewarded = locked
2525            .accounts
2526            .iter()
2527            .find(|item| item.node_id == "node-b")
2528            .unwrap();
2529        assert_eq!(rewarded.balance, evo.stake_policy.reuse_reward);
2530        assert!(
2531            locked.selector_reputation_bias()["node-b"]
2532                > locked.selector_reputation_bias()["node-a"]
2533        );
2534    }
2535
2536    #[tokio::test]
2537    async fn remote_reuse_failure_penalizes_remote_reputation() {
2538        let ledger = Arc::new(Mutex::new(EvuLedger::default()));
2539        let failing_validator: Arc<dyn Validator> = Arc::new(FixedValidator { success: false });
2540        let (evo, _) = build_test_evo("remote-failure", "run-failure", failing_validator);
2541        let evo = evo.with_economics(ledger.clone());
2542
2543        let envelope = remote_publish_envelope(
2544            "node-remote",
2545            "run-remote-failed",
2546            "gene-remote",
2547            "capsule-remote",
2548            "mutation-remote",
2549            "failure-signal",
2550            "FAILED.md",
2551            "# from remote",
2552        );
2553        evo.import_remote_envelope(&envelope).unwrap();
2554
2555        let decision = evo
2556            .replay_or_fallback(replay_input("failure-signal"))
2557            .await
2558            .unwrap();
2559
2560        assert!(!decision.used_capsule);
2561        assert!(decision.fallback_to_planner);
2562
2563        let signal = evo.economics_signal("node-remote").unwrap();
2564        assert_eq!(signal.available_evu, 0);
2565        assert!(signal.publish_success_rate < 0.5);
2566        assert!(signal.validator_accuracy < 0.5);
2567    }
2568}