1use 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}