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