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