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