1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::fs::{self, File, OpenOptions};
5use std::io::{BufRead, BufReader, Write};
6use std::path::{Path, PathBuf};
7use std::sync::Mutex;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use chrono::{DateTime, Duration, Utc};
11use oris_kernel::RunId;
12use serde::{Deserialize, Serialize};
13use sha2::{Digest, Sha256};
14use thiserror::Error;
15
16pub type MutationId = String;
17pub type GeneId = String;
18pub type CapsuleId = String;
19
20pub const REPLAY_CONFIDENCE_DECAY_RATE_PER_HOUR: f32 = 0.05;
21pub const MIN_REPLAY_CONFIDENCE: f32 = 0.35;
22
23#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
24pub enum AssetState {
25 Candidate,
26 #[default]
27 Promoted,
28 Revoked,
29 Archived,
30 Quarantined,
31}
32
33pub fn asset_state_to_evomap_compat(state: &AssetState) -> &'static str {
36 match state {
37 AssetState::Candidate => "candidate",
38 AssetState::Promoted => "promoted",
39 AssetState::Revoked => "revoked",
40 AssetState::Archived => "rejected", AssetState::Quarantined => "quarantined",
42 }
43}
44
45#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
46pub enum CandidateSource {
47 #[default]
48 Local,
49 Remote,
50}
51
52#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "snake_case")]
54pub enum TransitionReasonCode {
55 #[default]
56 Unspecified,
57 PromotionSuccessThreshold,
58 PromotionRemoteReplayValidated,
59 PromotionBuiltinColdStartCompatibility,
60 PromotionTrustedLocalReport,
61 RevalidationConfidenceDecay,
62 DowngradeReplayRegression,
63 DowngradeConfidenceRegression,
64 DowngradeRemoteRequiresLocalValidation,
65 DowngradeBootstrapRequiresLocalValidation,
66 DowngradeBuiltinRequiresValidation,
67 CandidateRateLimited,
68 CandidateCoolingWindow,
69 CandidateBlastRadiusExceeded,
70 CandidateCollectingEvidence,
71}
72
73#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
74pub struct BlastRadius {
75 pub files_changed: usize,
76 pub lines_changed: usize,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
80pub enum RiskLevel {
81 Low,
82 Medium,
83 High,
84}
85
86#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
87pub enum ArtifactEncoding {
88 UnifiedDiff,
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
92pub enum MutationTarget {
93 WorkspaceRoot,
94 Crate { name: String },
95 Paths { allow: Vec<String> },
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
99pub struct MutationIntent {
100 pub id: MutationId,
101 pub intent: String,
102 pub target: MutationTarget,
103 pub expected_effect: String,
104 pub risk: RiskLevel,
105 pub signals: Vec<String>,
106 #[serde(default)]
107 pub spec_id: Option<String>,
108}
109
110#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
111pub struct MutationArtifact {
112 pub encoding: ArtifactEncoding,
113 pub payload: String,
114 pub base_revision: Option<String>,
115 pub content_hash: String,
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
119pub struct PreparedMutation {
120 pub intent: MutationIntent,
121 pub artifact: MutationArtifact,
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
125pub struct ValidationSnapshot {
126 pub success: bool,
127 pub profile: String,
128 pub duration_ms: u64,
129 pub summary: String,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
133pub struct Outcome {
134 pub success: bool,
135 pub validation_profile: String,
136 pub validation_duration_ms: u64,
137 pub changed_files: Vec<String>,
138 pub validator_hash: String,
139 #[serde(default)]
140 pub lines_changed: usize,
141 #[serde(default)]
142 pub replay_verified: bool,
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
146pub struct EnvFingerprint {
147 pub rustc_version: String,
148 pub cargo_lock_hash: String,
149 pub target_triple: String,
150 pub os: String,
151}
152
153#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
154pub struct Capsule {
155 pub id: CapsuleId,
156 pub gene_id: GeneId,
157 pub mutation_id: MutationId,
158 pub run_id: RunId,
159 pub diff_hash: String,
160 pub confidence: f32,
161 pub env: EnvFingerprint,
162 pub outcome: Outcome,
163 #[serde(default)]
164 pub state: AssetState,
165}
166
167#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
168pub struct Gene {
169 pub id: GeneId,
170 pub signals: Vec<String>,
171 pub strategy: Vec<String>,
172 pub validation: Vec<String>,
173 #[serde(default)]
174 pub state: AssetState,
175}
176
177#[derive(Clone, Debug, Serialize, Deserialize)]
178#[serde(tag = "kind", rename_all = "snake_case")]
179pub enum EvolutionEvent {
180 MutationDeclared {
181 mutation: PreparedMutation,
182 },
183 MutationApplied {
184 mutation_id: MutationId,
185 patch_hash: String,
186 changed_files: Vec<String>,
187 },
188 SignalsExtracted {
189 mutation_id: MutationId,
190 hash: String,
191 signals: Vec<String>,
192 },
193 MutationRejected {
194 mutation_id: MutationId,
195 reason: String,
196 },
197 ValidationPassed {
198 mutation_id: MutationId,
199 report: ValidationSnapshot,
200 gene_id: Option<GeneId>,
201 },
202 ValidationFailed {
203 mutation_id: MutationId,
204 report: ValidationSnapshot,
205 gene_id: Option<GeneId>,
206 },
207 CapsuleCommitted {
208 capsule: Capsule,
209 },
210 CapsuleQuarantined {
211 capsule_id: CapsuleId,
212 },
213 CapsuleReleased {
214 capsule_id: CapsuleId,
215 state: AssetState,
216 },
217 CapsuleReused {
218 capsule_id: CapsuleId,
219 gene_id: GeneId,
220 run_id: RunId,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 replay_run_id: Option<RunId>,
223 },
224 GeneProjected {
225 gene: Gene,
226 },
227 GenePromoted {
228 gene_id: GeneId,
229 },
230 GeneRevoked {
231 gene_id: GeneId,
232 reason: String,
233 },
234 GeneArchived {
235 gene_id: GeneId,
236 },
237 PromotionEvaluated {
238 gene_id: GeneId,
239 state: AssetState,
240 reason: String,
241 #[serde(default)]
242 reason_code: TransitionReasonCode,
243 },
244 RemoteAssetImported {
245 source: CandidateSource,
246 asset_ids: Vec<String>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 sender_id: Option<String>,
249 },
250 ManifestValidated {
251 accepted: bool,
252 reason: String,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 sender_id: Option<String>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 publisher: Option<String>,
257 #[serde(default)]
258 asset_ids: Vec<String>,
259 },
260 SpecLinked {
261 mutation_id: MutationId,
262 spec_id: String,
263 },
264}
265
266#[derive(Clone, Debug, Serialize, Deserialize)]
267pub struct StoredEvolutionEvent {
268 pub seq: u64,
269 pub timestamp: String,
270 pub prev_hash: String,
271 pub record_hash: String,
272 pub event: EvolutionEvent,
273}
274
275#[derive(Clone, Debug, Default, Serialize, Deserialize)]
276pub struct EvolutionProjection {
277 pub genes: Vec<Gene>,
278 pub capsules: Vec<Capsule>,
279 pub reuse_counts: BTreeMap<GeneId, u64>,
280 pub attempt_counts: BTreeMap<GeneId, u64>,
281 pub last_updated_at: BTreeMap<GeneId, String>,
282 pub spec_ids_by_gene: BTreeMap<GeneId, BTreeSet<String>>,
283}
284
285#[derive(Clone, Debug)]
286pub struct SelectorInput {
287 pub signals: Vec<String>,
288 pub env: EnvFingerprint,
289 pub spec_id: Option<String>,
290 pub limit: usize,
291}
292
293#[derive(Clone, Debug)]
294pub struct GeneCandidate {
295 pub gene: Gene,
296 pub score: f32,
297 pub capsules: Vec<Capsule>,
298}
299
300pub trait Selector: Send + Sync {
301 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate>;
302}
303
304pub trait EvolutionStore: Send + Sync {
305 fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError>;
306 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError>;
307 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError>;
308
309 fn scan_projection(
310 &self,
311 ) -> Result<(Vec<StoredEvolutionEvent>, EvolutionProjection), EvolutionError> {
312 let events = self.scan(1)?;
313 let projection = rebuild_projection_from_events(&events);
314 Ok((events, projection))
315 }
316}
317
318#[derive(Debug, Error)]
319pub enum EvolutionError {
320 #[error("I/O error: {0}")]
321 Io(String),
322 #[error("Serialization error: {0}")]
323 Serde(String),
324 #[error("Hash chain validation failed: {0}")]
325 HashChain(String),
326}
327
328pub struct JsonlEvolutionStore {
329 root_dir: PathBuf,
330 lock: Mutex<()>,
331}
332
333impl JsonlEvolutionStore {
334 pub fn new<P: Into<PathBuf>>(root_dir: P) -> Self {
335 Self {
336 root_dir: root_dir.into(),
337 lock: Mutex::new(()),
338 }
339 }
340
341 pub fn root_dir(&self) -> &Path {
342 &self.root_dir
343 }
344
345 fn ensure_layout(&self) -> Result<(), EvolutionError> {
346 fs::create_dir_all(&self.root_dir).map_err(io_err)?;
347 let lock_path = self.root_dir.join("LOCK");
348 if !lock_path.exists() {
349 File::create(lock_path).map_err(io_err)?;
350 }
351 let events_path = self.events_path();
352 if !events_path.exists() {
353 File::create(events_path).map_err(io_err)?;
354 }
355 Ok(())
356 }
357
358 fn events_path(&self) -> PathBuf {
359 self.root_dir.join("events.jsonl")
360 }
361
362 fn genes_path(&self) -> PathBuf {
363 self.root_dir.join("genes.json")
364 }
365
366 fn capsules_path(&self) -> PathBuf {
367 self.root_dir.join("capsules.json")
368 }
369
370 fn read_all_events(&self) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
371 self.ensure_layout()?;
372 let file = File::open(self.events_path()).map_err(io_err)?;
373 let reader = BufReader::new(file);
374 let mut events = Vec::new();
375 for line in reader.lines() {
376 let line = line.map_err(io_err)?;
377 if line.trim().is_empty() {
378 continue;
379 }
380 let event = serde_json::from_str::<StoredEvolutionEvent>(&line)
381 .map_err(|err| EvolutionError::Serde(err.to_string()))?;
382 events.push(event);
383 }
384 verify_hash_chain(&events)?;
385 Ok(events)
386 }
387
388 fn write_projection_files(
389 &self,
390 projection: &EvolutionProjection,
391 ) -> Result<(), EvolutionError> {
392 write_json_atomic(&self.genes_path(), &projection.genes)?;
393 write_json_atomic(&self.capsules_path(), &projection.capsules)?;
394 Ok(())
395 }
396
397 fn append_event_locked(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
398 let existing = self.read_all_events()?;
399 let next_seq = existing.last().map(|entry| entry.seq + 1).unwrap_or(1);
400 let prev_hash = existing
401 .last()
402 .map(|entry| entry.record_hash.clone())
403 .unwrap_or_default();
404 let timestamp = Utc::now().to_rfc3339();
405 let record_hash = hash_record(next_seq, ×tamp, &prev_hash, &event)?;
406 let stored = StoredEvolutionEvent {
407 seq: next_seq,
408 timestamp,
409 prev_hash,
410 record_hash,
411 event,
412 };
413 let mut file = OpenOptions::new()
414 .create(true)
415 .append(true)
416 .open(self.events_path())
417 .map_err(io_err)?;
418 let line =
419 serde_json::to_string(&stored).map_err(|err| EvolutionError::Serde(err.to_string()))?;
420 file.write_all(line.as_bytes()).map_err(io_err)?;
421 file.write_all(b"\n").map_err(io_err)?;
422 file.sync_data().map_err(io_err)?;
423
424 let events = self.read_all_events()?;
425 let projection = rebuild_projection_from_events(&events);
426 self.write_projection_files(&projection)?;
427 Ok(next_seq)
428 }
429}
430
431impl EvolutionStore for JsonlEvolutionStore {
432 fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
433 let _guard = self
434 .lock
435 .lock()
436 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
437 self.append_event_locked(event)
438 }
439
440 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
441 let _guard = self
442 .lock
443 .lock()
444 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
445 Ok(self
446 .read_all_events()?
447 .into_iter()
448 .filter(|entry| entry.seq >= from_seq)
449 .collect())
450 }
451
452 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
453 let _guard = self
454 .lock
455 .lock()
456 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
457 let projection = rebuild_projection_from_events(&self.read_all_events()?);
458 self.write_projection_files(&projection)?;
459 Ok(projection)
460 }
461
462 fn scan_projection(
463 &self,
464 ) -> Result<(Vec<StoredEvolutionEvent>, EvolutionProjection), EvolutionError> {
465 let _guard = self
466 .lock
467 .lock()
468 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
469 let events = self.read_all_events()?;
470 let projection = rebuild_projection_from_events(&events);
471 self.write_projection_files(&projection)?;
472 Ok((events, projection))
473 }
474}
475
476pub struct ProjectionSelector {
477 projection: EvolutionProjection,
478 now: DateTime<Utc>,
479}
480
481impl ProjectionSelector {
482 pub fn new(projection: EvolutionProjection) -> Self {
483 Self {
484 projection,
485 now: Utc::now(),
486 }
487 }
488
489 pub fn with_now(projection: EvolutionProjection, now: DateTime<Utc>) -> Self {
490 Self { projection, now }
491 }
492}
493
494impl Selector for ProjectionSelector {
495 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate> {
496 let requested_spec_id = input
497 .spec_id
498 .as_deref()
499 .map(str::trim)
500 .filter(|value| !value.is_empty());
501 let mut out = Vec::new();
502 for gene in &self.projection.genes {
503 if gene.state != AssetState::Promoted {
504 continue;
505 }
506 if let Some(spec_id) = requested_spec_id {
507 let matches_spec = self
508 .projection
509 .spec_ids_by_gene
510 .get(&gene.id)
511 .map(|values| {
512 values
513 .iter()
514 .any(|value| value.eq_ignore_ascii_case(spec_id))
515 })
516 .unwrap_or(false);
517 if !matches_spec {
518 continue;
519 }
520 }
521 let capsules = self
522 .projection
523 .capsules
524 .iter()
525 .filter(|capsule| {
526 capsule.gene_id == gene.id && capsule.state == AssetState::Promoted
527 })
528 .cloned()
529 .collect::<Vec<_>>();
530 if capsules.is_empty() {
531 continue;
532 }
533 let mut capsules = capsules;
534 capsules.sort_by(|left, right| {
535 environment_match_factor(&input.env, &right.env)
536 .partial_cmp(&environment_match_factor(&input.env, &left.env))
537 .unwrap_or(std::cmp::Ordering::Equal)
538 .then_with(|| {
539 right
540 .confidence
541 .partial_cmp(&left.confidence)
542 .unwrap_or(std::cmp::Ordering::Equal)
543 })
544 .then_with(|| left.id.cmp(&right.id))
545 });
546 let env_match_factor = capsules
547 .first()
548 .map(|capsule| environment_match_factor(&input.env, &capsule.env))
549 .unwrap_or(0.0);
550
551 let successful_capsules = capsules.len() as f64;
552 let attempts = self
553 .projection
554 .attempt_counts
555 .get(&gene.id)
556 .copied()
557 .unwrap_or(capsules.len() as u64) as f64;
558 let success_rate = if attempts == 0.0 {
559 0.0
560 } else {
561 successful_capsules / attempts
562 };
563 let successful_reuses = self
564 .projection
565 .reuse_counts
566 .get(&gene.id)
567 .copied()
568 .unwrap_or(0) as f64;
569 let reuse_count_factor = 1.0 + (1.0 + successful_reuses).ln();
570 let signal_overlap = normalized_signal_overlap(&gene.signals, &input.signals);
571 let age_secs = self
572 .projection
573 .last_updated_at
574 .get(&gene.id)
575 .and_then(|value| seconds_since_timestamp(value, self.now));
576 let peak_confidence = capsules
577 .iter()
578 .map(|capsule| capsule.confidence)
579 .fold(0.0_f32, f32::max) as f64;
580 let freshness_confidence = capsules
581 .iter()
582 .map(|capsule| decayed_replay_confidence(capsule.confidence, age_secs))
583 .fold(0.0_f32, f32::max) as f64;
584 if freshness_confidence < MIN_REPLAY_CONFIDENCE as f64 {
585 continue;
586 }
587 let freshness_factor = if peak_confidence <= 0.0 {
588 0.0
589 } else {
590 (freshness_confidence / peak_confidence).clamp(0.0, 1.0)
591 };
592 let score = (success_rate
593 * reuse_count_factor
594 * env_match_factor
595 * freshness_factor
596 * signal_overlap) as f32;
597 if score < 0.35 {
598 continue;
599 }
600 out.push(GeneCandidate {
601 gene: gene.clone(),
602 score,
603 capsules,
604 });
605 }
606
607 out.sort_by(|left, right| {
608 right
609 .score
610 .partial_cmp(&left.score)
611 .unwrap_or(std::cmp::Ordering::Equal)
612 .then_with(|| left.gene.id.cmp(&right.gene.id))
613 });
614 out.truncate(input.limit.max(1));
615 out
616 }
617}
618
619pub struct StoreBackedSelector {
620 store: std::sync::Arc<dyn EvolutionStore>,
621}
622
623impl StoreBackedSelector {
624 pub fn new(store: std::sync::Arc<dyn EvolutionStore>) -> Self {
625 Self { store }
626 }
627}
628
629impl Selector for StoreBackedSelector {
630 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate> {
631 match self.store.scan_projection() {
632 Ok((_, projection)) => ProjectionSelector::new(projection).select(input),
633 Err(_) => Vec::new(),
634 }
635 }
636}
637
638pub fn rebuild_projection_from_events(events: &[StoredEvolutionEvent]) -> EvolutionProjection {
639 let mut genes = BTreeMap::<GeneId, Gene>::new();
640 let mut capsules = BTreeMap::<CapsuleId, Capsule>::new();
641 let mut reuse_counts = BTreeMap::<GeneId, u64>::new();
642 let mut attempt_counts = BTreeMap::<GeneId, u64>::new();
643 let mut last_updated_at = BTreeMap::<GeneId, String>::new();
644 let mut spec_ids_by_gene = BTreeMap::<GeneId, BTreeSet<String>>::new();
645 let mut mutation_to_gene = HashMap::<MutationId, GeneId>::new();
646 let mut mutation_spec_ids = HashMap::<MutationId, String>::new();
647
648 for stored in events {
649 match &stored.event {
650 EvolutionEvent::MutationDeclared { mutation } => {
651 if let Some(spec_id) = mutation
652 .intent
653 .spec_id
654 .as_ref()
655 .map(|value| value.trim())
656 .filter(|value| !value.is_empty())
657 {
658 mutation_spec_ids.insert(mutation.intent.id.clone(), spec_id.to_string());
659 if let Some(gene_id) = mutation_to_gene.get(&mutation.intent.id) {
660 spec_ids_by_gene
661 .entry(gene_id.clone())
662 .or_default()
663 .insert(spec_id.to_string());
664 }
665 }
666 }
667 EvolutionEvent::SpecLinked {
668 mutation_id,
669 spec_id,
670 } => {
671 let spec_id = spec_id.trim();
672 if !spec_id.is_empty() {
673 mutation_spec_ids.insert(mutation_id.clone(), spec_id.to_string());
674 if let Some(gene_id) = mutation_to_gene.get(mutation_id) {
675 spec_ids_by_gene
676 .entry(gene_id.clone())
677 .or_default()
678 .insert(spec_id.to_string());
679 }
680 }
681 }
682 EvolutionEvent::GeneProjected { gene } => {
683 genes.insert(gene.id.clone(), gene.clone());
684 last_updated_at.insert(gene.id.clone(), stored.timestamp.clone());
685 }
686 EvolutionEvent::GenePromoted { gene_id } => {
687 if let Some(gene) = genes.get_mut(gene_id) {
688 gene.state = AssetState::Promoted;
689 }
690 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
691 }
692 EvolutionEvent::GeneRevoked { gene_id, .. } => {
693 if let Some(gene) = genes.get_mut(gene_id) {
694 gene.state = AssetState::Revoked;
695 }
696 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
697 }
698 EvolutionEvent::GeneArchived { gene_id } => {
699 if let Some(gene) = genes.get_mut(gene_id) {
700 gene.state = AssetState::Archived;
701 }
702 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
703 }
704 EvolutionEvent::PromotionEvaluated { gene_id, state, .. } => {
705 if let Some(gene) = genes.get_mut(gene_id) {
706 gene.state = state.clone();
707 }
708 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
709 }
710 EvolutionEvent::CapsuleCommitted { capsule } => {
711 mutation_to_gene.insert(capsule.mutation_id.clone(), capsule.gene_id.clone());
712 capsules.insert(capsule.id.clone(), capsule.clone());
713 *attempt_counts.entry(capsule.gene_id.clone()).or_insert(0) += 1;
714 if let Some(spec_id) = mutation_spec_ids.get(&capsule.mutation_id) {
715 spec_ids_by_gene
716 .entry(capsule.gene_id.clone())
717 .or_default()
718 .insert(spec_id.clone());
719 }
720 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
721 }
722 EvolutionEvent::CapsuleQuarantined { capsule_id } => {
723 if let Some(capsule) = capsules.get_mut(capsule_id) {
724 capsule.state = AssetState::Quarantined;
725 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
726 }
727 }
728 EvolutionEvent::CapsuleReleased { capsule_id, state } => {
729 if let Some(capsule) = capsules.get_mut(capsule_id) {
730 capsule.state = state.clone();
731 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
732 }
733 }
734 EvolutionEvent::CapsuleReused { gene_id, .. } => {
735 *reuse_counts.entry(gene_id.clone()).or_insert(0) += 1;
736 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
737 }
738 EvolutionEvent::ValidationFailed {
739 mutation_id,
740 gene_id,
741 ..
742 } => {
743 let id = gene_id
744 .clone()
745 .or_else(|| mutation_to_gene.get(mutation_id).cloned());
746 if let Some(gene_id) = id {
747 *attempt_counts.entry(gene_id.clone()).or_insert(0) += 1;
748 last_updated_at.insert(gene_id, stored.timestamp.clone());
749 }
750 }
751 EvolutionEvent::ValidationPassed {
752 mutation_id,
753 gene_id,
754 ..
755 } => {
756 let id = gene_id
757 .clone()
758 .or_else(|| mutation_to_gene.get(mutation_id).cloned());
759 if let Some(gene_id) = id {
760 *attempt_counts.entry(gene_id.clone()).or_insert(0) += 1;
761 last_updated_at.insert(gene_id, stored.timestamp.clone());
762 }
763 }
764 _ => {}
765 }
766 }
767
768 EvolutionProjection {
769 genes: genes.into_values().collect(),
770 capsules: capsules.into_values().collect(),
771 reuse_counts,
772 attempt_counts,
773 last_updated_at,
774 spec_ids_by_gene,
775 }
776}
777
778pub fn default_store_root() -> PathBuf {
779 PathBuf::from(".oris").join("evolution")
780}
781
782pub fn hash_string(input: &str) -> String {
783 let mut hasher = Sha256::new();
784 hasher.update(input.as_bytes());
785 hex::encode(hasher.finalize())
786}
787
788pub fn stable_hash_json<T: Serialize>(value: &T) -> Result<String, EvolutionError> {
789 let bytes = serde_json::to_vec(value).map_err(|err| EvolutionError::Serde(err.to_string()))?;
790 let mut hasher = Sha256::new();
791 hasher.update(bytes);
792 Ok(hex::encode(hasher.finalize()))
793}
794
795pub fn compute_artifact_hash(payload: &str) -> String {
796 hash_string(payload)
797}
798
799pub fn next_id(prefix: &str) -> String {
800 let nanos = SystemTime::now()
801 .duration_since(UNIX_EPOCH)
802 .unwrap_or_default()
803 .as_nanos();
804 format!("{prefix}-{nanos:x}")
805}
806
807pub fn decayed_replay_confidence(confidence: f32, age_secs: Option<u64>) -> f32 {
808 if confidence <= 0.0 {
809 return 0.0;
810 }
811 let age_hours = age_secs.unwrap_or(0) as f32 / 3600.0;
812 let decay = (-REPLAY_CONFIDENCE_DECAY_RATE_PER_HOUR * age_hours).exp();
813 (confidence * decay).clamp(0.0, 1.0)
814}
815
816fn normalized_signal_overlap(gene_signals: &[String], input_signals: &[String]) -> f64 {
817 let gene = canonical_signal_phrases(gene_signals);
818 let input = canonical_signal_phrases(input_signals);
819 if input.is_empty() || gene.is_empty() {
820 return 0.0;
821 }
822 let matched = input
823 .iter()
824 .map(|signal| best_signal_match(&gene, signal))
825 .sum::<f64>();
826 matched / input.len() as f64
827}
828
829#[derive(Clone, Debug, PartialEq, Eq)]
830struct CanonicalSignal {
831 phrase: String,
832 tokens: BTreeSet<String>,
833}
834
835fn canonical_signal_phrases(signals: &[String]) -> Vec<CanonicalSignal> {
836 signals
837 .iter()
838 .filter_map(|signal| canonical_signal_phrase(signal))
839 .collect()
840}
841
842fn canonical_signal_phrase(input: &str) -> Option<CanonicalSignal> {
843 let tokens = input
844 .split(|ch: char| !ch.is_ascii_alphanumeric())
845 .map(|token| token.trim().to_ascii_lowercase())
846 .filter(|token| token.len() >= 3)
847 .collect::<BTreeSet<_>>();
848 if tokens.is_empty() {
849 return None;
850 }
851 let phrase = tokens.iter().cloned().collect::<Vec<_>>().join(" ");
852 Some(CanonicalSignal { phrase, tokens })
853}
854
855fn best_signal_match(gene_signals: &[CanonicalSignal], input: &CanonicalSignal) -> f64 {
856 gene_signals
857 .iter()
858 .map(|candidate| deterministic_phrase_match(candidate, input))
859 .fold(0.0, f64::max)
860}
861
862fn deterministic_phrase_match(candidate: &CanonicalSignal, input: &CanonicalSignal) -> f64 {
863 if candidate.phrase == input.phrase {
864 return 1.0;
865 }
866 if candidate.tokens.len() < 2 || input.tokens.len() < 2 {
867 return 0.0;
868 }
869 let shared = candidate.tokens.intersection(&input.tokens).count();
870 if shared < 2 {
871 return 0.0;
872 }
873 let overlap = shared as f64 / candidate.tokens.len().min(input.tokens.len()) as f64;
874 if overlap >= 0.67 {
875 overlap
876 } else {
877 0.0
878 }
879}
880fn seconds_since_timestamp(timestamp: &str, now: DateTime<Utc>) -> Option<u64> {
881 let parsed = DateTime::parse_from_rfc3339(timestamp)
882 .ok()?
883 .with_timezone(&Utc);
884 let elapsed = now.signed_duration_since(parsed);
885 if elapsed < Duration::zero() {
886 Some(0)
887 } else {
888 u64::try_from(elapsed.num_seconds()).ok()
889 }
890}
891fn environment_match_factor(input: &EnvFingerprint, candidate: &EnvFingerprint) -> f64 {
892 let fields = [
893 input
894 .rustc_version
895 .eq_ignore_ascii_case(&candidate.rustc_version),
896 input
897 .cargo_lock_hash
898 .eq_ignore_ascii_case(&candidate.cargo_lock_hash),
899 input
900 .target_triple
901 .eq_ignore_ascii_case(&candidate.target_triple),
902 input.os.eq_ignore_ascii_case(&candidate.os),
903 ];
904 let matched_fields = fields.into_iter().filter(|matched| *matched).count() as f64;
905 0.5 + ((matched_fields / 4.0) * 0.5)
906}
907
908fn hash_record(
909 seq: u64,
910 timestamp: &str,
911 prev_hash: &str,
912 event: &EvolutionEvent,
913) -> Result<String, EvolutionError> {
914 stable_hash_json(&(seq, timestamp, prev_hash, event))
915}
916
917fn verify_hash_chain(events: &[StoredEvolutionEvent]) -> Result<(), EvolutionError> {
918 let mut previous_hash = String::new();
919 let mut expected_seq = 1u64;
920 for event in events {
921 if event.seq != expected_seq {
922 return Err(EvolutionError::HashChain(format!(
923 "expected seq {}, found {}",
924 expected_seq, event.seq
925 )));
926 }
927 if event.prev_hash != previous_hash {
928 return Err(EvolutionError::HashChain(format!(
929 "event {} prev_hash mismatch",
930 event.seq
931 )));
932 }
933 let actual_hash = hash_record(event.seq, &event.timestamp, &event.prev_hash, &event.event)?;
934 if actual_hash != event.record_hash {
935 return Err(EvolutionError::HashChain(format!(
936 "event {} record_hash mismatch",
937 event.seq
938 )));
939 }
940 previous_hash = event.record_hash.clone();
941 expected_seq += 1;
942 }
943 Ok(())
944}
945
946fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), EvolutionError> {
947 let tmp_path = path.with_extension("tmp");
948 let bytes =
949 serde_json::to_vec_pretty(value).map_err(|err| EvolutionError::Serde(err.to_string()))?;
950 fs::write(&tmp_path, bytes).map_err(io_err)?;
951 fs::rename(&tmp_path, path).map_err(io_err)?;
952 Ok(())
953}
954
955fn io_err(err: std::io::Error) -> EvolutionError {
956 EvolutionError::Io(err.to_string())
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962
963 fn temp_root(name: &str) -> PathBuf {
964 std::env::temp_dir().join(format!("oris-evolution-{name}-{}", next_id("t")))
965 }
966
967 fn sample_mutation() -> PreparedMutation {
968 PreparedMutation {
969 intent: MutationIntent {
970 id: "mutation-1".into(),
971 intent: "tighten borrow scope".into(),
972 target: MutationTarget::Paths {
973 allow: vec!["crates/oris-kernel".into()],
974 },
975 expected_effect: "cargo check passes".into(),
976 risk: RiskLevel::Low,
977 signals: vec!["rust borrow error".into()],
978 spec_id: None,
979 },
980 artifact: MutationArtifact {
981 encoding: ArtifactEncoding::UnifiedDiff,
982 payload: "diff --git a/foo b/foo".into(),
983 base_revision: Some("HEAD".into()),
984 content_hash: compute_artifact_hash("diff --git a/foo b/foo"),
985 },
986 }
987 }
988
989 #[test]
990 fn evomap_asset_state_mapping_archived_is_rejected() {
991 assert_eq!(
992 asset_state_to_evomap_compat(&AssetState::Archived),
993 "rejected"
994 );
995 assert_eq!(
996 asset_state_to_evomap_compat(&AssetState::Quarantined),
997 "quarantined"
998 );
999 }
1000
1001 #[test]
1002 fn append_event_assigns_monotonic_seq() {
1003 let root = temp_root("seq");
1004 let store = JsonlEvolutionStore::new(root);
1005 let first = store
1006 .append_event(EvolutionEvent::MutationDeclared {
1007 mutation: sample_mutation(),
1008 })
1009 .unwrap();
1010 let second = store
1011 .append_event(EvolutionEvent::MutationRejected {
1012 mutation_id: "mutation-1".into(),
1013 reason: "no-op".into(),
1014 })
1015 .unwrap();
1016 assert_eq!(first, 1);
1017 assert_eq!(second, 2);
1018 }
1019
1020 #[test]
1021 fn tampered_hash_chain_is_rejected() {
1022 let root = temp_root("tamper");
1023 let store = JsonlEvolutionStore::new(&root);
1024 store
1025 .append_event(EvolutionEvent::MutationDeclared {
1026 mutation: sample_mutation(),
1027 })
1028 .unwrap();
1029 let path = root.join("events.jsonl");
1030 let contents = fs::read_to_string(&path).unwrap();
1031 let mutated = contents.replace("tighten borrow scope", "tampered");
1032 fs::write(&path, mutated).unwrap();
1033 let result = store.scan(1);
1034 assert!(matches!(result, Err(EvolutionError::HashChain(_))));
1035 }
1036
1037 #[test]
1038 fn rebuild_projection_after_cache_deletion() {
1039 let root = temp_root("projection");
1040 let store = JsonlEvolutionStore::new(&root);
1041 let gene = Gene {
1042 id: "gene-1".into(),
1043 signals: vec!["rust borrow error".into()],
1044 strategy: vec!["crates".into()],
1045 validation: vec!["oris-default".into()],
1046 state: AssetState::Promoted,
1047 };
1048 let capsule = Capsule {
1049 id: "capsule-1".into(),
1050 gene_id: gene.id.clone(),
1051 mutation_id: "mutation-1".into(),
1052 run_id: "run-1".into(),
1053 diff_hash: "abc".into(),
1054 confidence: 0.7,
1055 env: EnvFingerprint {
1056 rustc_version: "rustc 1.80".into(),
1057 cargo_lock_hash: "lock".into(),
1058 target_triple: "x86_64-unknown-linux-gnu".into(),
1059 os: "linux".into(),
1060 },
1061 outcome: Outcome {
1062 success: true,
1063 validation_profile: "oris-default".into(),
1064 validation_duration_ms: 100,
1065 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
1066 validator_hash: "vh".into(),
1067 lines_changed: 1,
1068 replay_verified: false,
1069 },
1070 state: AssetState::Promoted,
1071 };
1072 store
1073 .append_event(EvolutionEvent::GeneProjected { gene })
1074 .unwrap();
1075 store
1076 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
1077 .unwrap();
1078 fs::remove_file(root.join("genes.json")).unwrap();
1079 fs::remove_file(root.join("capsules.json")).unwrap();
1080 let projection = store.rebuild_projection().unwrap();
1081 assert_eq!(projection.genes.len(), 1);
1082 assert_eq!(projection.capsules.len(), 1);
1083 }
1084
1085 #[test]
1086 fn rebuild_projection_tracks_spec_ids_for_genes() {
1087 let root = temp_root("projection-spec");
1088 let store = JsonlEvolutionStore::new(&root);
1089 let mut mutation = sample_mutation();
1090 mutation.intent.id = "mutation-spec".into();
1091 mutation.intent.spec_id = Some("spec-repair-1".into());
1092 let gene = Gene {
1093 id: "gene-spec".into(),
1094 signals: vec!["rust borrow error".into()],
1095 strategy: vec!["crates".into()],
1096 validation: vec!["oris-default".into()],
1097 state: AssetState::Promoted,
1098 };
1099 let capsule = Capsule {
1100 id: "capsule-spec".into(),
1101 gene_id: gene.id.clone(),
1102 mutation_id: mutation.intent.id.clone(),
1103 run_id: "run-spec".into(),
1104 diff_hash: "abc".into(),
1105 confidence: 0.7,
1106 env: EnvFingerprint {
1107 rustc_version: "rustc 1.80".into(),
1108 cargo_lock_hash: "lock".into(),
1109 target_triple: "x86_64-unknown-linux-gnu".into(),
1110 os: "linux".into(),
1111 },
1112 outcome: Outcome {
1113 success: true,
1114 validation_profile: "oris-default".into(),
1115 validation_duration_ms: 100,
1116 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
1117 validator_hash: "vh".into(),
1118 lines_changed: 1,
1119 replay_verified: false,
1120 },
1121 state: AssetState::Promoted,
1122 };
1123 store
1124 .append_event(EvolutionEvent::MutationDeclared { mutation })
1125 .unwrap();
1126 store
1127 .append_event(EvolutionEvent::GeneProjected { gene })
1128 .unwrap();
1129 store
1130 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
1131 .unwrap();
1132
1133 let projection = store.rebuild_projection().unwrap();
1134 let spec_ids = projection.spec_ids_by_gene.get("gene-spec").unwrap();
1135 assert!(spec_ids.contains("spec-repair-1"));
1136 }
1137
1138 #[test]
1139 fn rebuild_projection_tracks_spec_ids_from_spec_linked_events() {
1140 let root = temp_root("projection-spec-linked");
1141 let store = JsonlEvolutionStore::new(&root);
1142 let mut mutation = sample_mutation();
1143 mutation.intent.id = "mutation-spec-linked".into();
1144 mutation.intent.spec_id = None;
1145 let gene = Gene {
1146 id: "gene-spec-linked".into(),
1147 signals: vec!["rust borrow error".into()],
1148 strategy: vec!["crates".into()],
1149 validation: vec!["oris-default".into()],
1150 state: AssetState::Promoted,
1151 };
1152 let capsule = Capsule {
1153 id: "capsule-spec-linked".into(),
1154 gene_id: gene.id.clone(),
1155 mutation_id: mutation.intent.id.clone(),
1156 run_id: "run-spec-linked".into(),
1157 diff_hash: "abc".into(),
1158 confidence: 0.7,
1159 env: EnvFingerprint {
1160 rustc_version: "rustc 1.80".into(),
1161 cargo_lock_hash: "lock".into(),
1162 target_triple: "x86_64-unknown-linux-gnu".into(),
1163 os: "linux".into(),
1164 },
1165 outcome: Outcome {
1166 success: true,
1167 validation_profile: "oris-default".into(),
1168 validation_duration_ms: 100,
1169 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
1170 validator_hash: "vh".into(),
1171 lines_changed: 1,
1172 replay_verified: false,
1173 },
1174 state: AssetState::Promoted,
1175 };
1176 store
1177 .append_event(EvolutionEvent::MutationDeclared { mutation })
1178 .unwrap();
1179 store
1180 .append_event(EvolutionEvent::GeneProjected { gene })
1181 .unwrap();
1182 store
1183 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
1184 .unwrap();
1185 store
1186 .append_event(EvolutionEvent::SpecLinked {
1187 mutation_id: "mutation-spec-linked".into(),
1188 spec_id: "spec-repair-linked".into(),
1189 })
1190 .unwrap();
1191
1192 let projection = store.rebuild_projection().unwrap();
1193 let spec_ids = projection.spec_ids_by_gene.get("gene-spec-linked").unwrap();
1194 assert!(spec_ids.contains("spec-repair-linked"));
1195 }
1196
1197 #[test]
1198 fn rebuild_projection_tracks_inline_spec_ids_even_when_declared_late() {
1199 let root = temp_root("projection-spec-inline-late");
1200 let store = JsonlEvolutionStore::new(&root);
1201 let mut mutation = sample_mutation();
1202 mutation.intent.id = "mutation-inline-late".into();
1203 mutation.intent.spec_id = Some("spec-inline-late".into());
1204 let gene = Gene {
1205 id: "gene-inline-late".into(),
1206 signals: vec!["rust borrow error".into()],
1207 strategy: vec!["crates".into()],
1208 validation: vec!["oris-default".into()],
1209 state: AssetState::Promoted,
1210 };
1211 let capsule = Capsule {
1212 id: "capsule-inline-late".into(),
1213 gene_id: gene.id.clone(),
1214 mutation_id: mutation.intent.id.clone(),
1215 run_id: "run-inline-late".into(),
1216 diff_hash: "abc".into(),
1217 confidence: 0.7,
1218 env: EnvFingerprint {
1219 rustc_version: "rustc 1.80".into(),
1220 cargo_lock_hash: "lock".into(),
1221 target_triple: "x86_64-unknown-linux-gnu".into(),
1222 os: "linux".into(),
1223 },
1224 outcome: Outcome {
1225 success: true,
1226 validation_profile: "oris-default".into(),
1227 validation_duration_ms: 100,
1228 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
1229 validator_hash: "vh".into(),
1230 lines_changed: 1,
1231 replay_verified: false,
1232 },
1233 state: AssetState::Promoted,
1234 };
1235 store
1236 .append_event(EvolutionEvent::GeneProjected { gene })
1237 .unwrap();
1238 store
1239 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
1240 .unwrap();
1241 store
1242 .append_event(EvolutionEvent::MutationDeclared { mutation })
1243 .unwrap();
1244
1245 let projection = store.rebuild_projection().unwrap();
1246 let spec_ids = projection.spec_ids_by_gene.get("gene-inline-late").unwrap();
1247 assert!(spec_ids.contains("spec-inline-late"));
1248 }
1249
1250 #[test]
1251 fn scan_projection_recreates_projection_files() {
1252 let root = temp_root("scan-projection");
1253 let store = JsonlEvolutionStore::new(&root);
1254 let mutation = sample_mutation();
1255 let gene = Gene {
1256 id: "gene-scan".into(),
1257 signals: vec!["rust borrow error".into()],
1258 strategy: vec!["crates".into()],
1259 validation: vec!["oris-default".into()],
1260 state: AssetState::Promoted,
1261 };
1262 let capsule = Capsule {
1263 id: "capsule-scan".into(),
1264 gene_id: gene.id.clone(),
1265 mutation_id: mutation.intent.id.clone(),
1266 run_id: "run-scan".into(),
1267 diff_hash: "abc".into(),
1268 confidence: 0.7,
1269 env: EnvFingerprint {
1270 rustc_version: "rustc 1.80".into(),
1271 cargo_lock_hash: "lock".into(),
1272 target_triple: "x86_64-unknown-linux-gnu".into(),
1273 os: "linux".into(),
1274 },
1275 outcome: Outcome {
1276 success: true,
1277 validation_profile: "oris-default".into(),
1278 validation_duration_ms: 100,
1279 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
1280 validator_hash: "vh".into(),
1281 lines_changed: 1,
1282 replay_verified: false,
1283 },
1284 state: AssetState::Promoted,
1285 };
1286 store
1287 .append_event(EvolutionEvent::MutationDeclared { mutation })
1288 .unwrap();
1289 store
1290 .append_event(EvolutionEvent::GeneProjected { gene })
1291 .unwrap();
1292 store
1293 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
1294 .unwrap();
1295 fs::remove_file(root.join("genes.json")).unwrap();
1296 fs::remove_file(root.join("capsules.json")).unwrap();
1297
1298 let (events, projection) = store.scan_projection().unwrap();
1299
1300 assert_eq!(events.len(), 3);
1301 assert_eq!(projection.genes.len(), 1);
1302 assert_eq!(projection.capsules.len(), 1);
1303 assert!(root.join("genes.json").exists());
1304 assert!(root.join("capsules.json").exists());
1305 }
1306
1307 #[test]
1308 fn default_scan_projection_uses_single_event_snapshot() {
1309 struct InconsistentSnapshotStore {
1310 scanned_events: Vec<StoredEvolutionEvent>,
1311 rebuilt_projection: EvolutionProjection,
1312 }
1313
1314 impl EvolutionStore for InconsistentSnapshotStore {
1315 fn append_event(&self, _event: EvolutionEvent) -> Result<u64, EvolutionError> {
1316 Err(EvolutionError::Io("unused in test".into()))
1317 }
1318
1319 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
1320 Ok(self
1321 .scanned_events
1322 .iter()
1323 .filter(|stored| stored.seq >= from_seq)
1324 .cloned()
1325 .collect())
1326 }
1327
1328 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
1329 Ok(self.rebuilt_projection.clone())
1330 }
1331 }
1332
1333 let scanned_gene = Gene {
1334 id: "gene-scanned".into(),
1335 signals: vec!["signal".into()],
1336 strategy: vec!["a".into()],
1337 validation: vec!["oris-default".into()],
1338 state: AssetState::Promoted,
1339 };
1340 let store = InconsistentSnapshotStore {
1341 scanned_events: vec![StoredEvolutionEvent {
1342 seq: 1,
1343 timestamp: "2026-03-04T00:00:00Z".into(),
1344 prev_hash: String::new(),
1345 record_hash: "hash".into(),
1346 event: EvolutionEvent::GeneProjected {
1347 gene: scanned_gene.clone(),
1348 },
1349 }],
1350 rebuilt_projection: EvolutionProjection {
1351 genes: vec![Gene {
1352 id: "gene-rebuilt".into(),
1353 signals: vec!["other".into()],
1354 strategy: vec!["b".into()],
1355 validation: vec!["oris-default".into()],
1356 state: AssetState::Promoted,
1357 }],
1358 ..Default::default()
1359 },
1360 };
1361
1362 let (events, projection) = store.scan_projection().unwrap();
1363
1364 assert_eq!(events.len(), 1);
1365 assert_eq!(projection.genes.len(), 1);
1366 assert_eq!(projection.genes[0].id, scanned_gene.id);
1367 }
1368
1369 #[test]
1370 fn store_backed_selector_uses_scan_projection_contract() {
1371 struct InconsistentSnapshotStore {
1372 scanned_events: Vec<StoredEvolutionEvent>,
1373 rebuilt_projection: EvolutionProjection,
1374 }
1375
1376 impl EvolutionStore for InconsistentSnapshotStore {
1377 fn append_event(&self, _event: EvolutionEvent) -> Result<u64, EvolutionError> {
1378 Err(EvolutionError::Io("unused in test".into()))
1379 }
1380
1381 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
1382 Ok(self
1383 .scanned_events
1384 .iter()
1385 .filter(|stored| stored.seq >= from_seq)
1386 .cloned()
1387 .collect())
1388 }
1389
1390 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
1391 Ok(self.rebuilt_projection.clone())
1392 }
1393 }
1394
1395 let scanned_gene = Gene {
1396 id: "gene-scanned".into(),
1397 signals: vec!["signal".into()],
1398 strategy: vec!["a".into()],
1399 validation: vec!["oris-default".into()],
1400 state: AssetState::Promoted,
1401 };
1402 let scanned_capsule = Capsule {
1403 id: "capsule-scanned".into(),
1404 gene_id: scanned_gene.id.clone(),
1405 mutation_id: "mutation-scanned".into(),
1406 run_id: "run-scanned".into(),
1407 diff_hash: "hash".into(),
1408 confidence: 0.8,
1409 env: EnvFingerprint {
1410 rustc_version: "rustc 1.80".into(),
1411 cargo_lock_hash: "lock".into(),
1412 target_triple: "x86_64-unknown-linux-gnu".into(),
1413 os: "linux".into(),
1414 },
1415 outcome: Outcome {
1416 success: true,
1417 validation_profile: "oris-default".into(),
1418 validation_duration_ms: 100,
1419 changed_files: vec!["file.rs".into()],
1420 validator_hash: "validator".into(),
1421 lines_changed: 1,
1422 replay_verified: false,
1423 },
1424 state: AssetState::Promoted,
1425 };
1426 let fresh_ts = Utc::now().to_rfc3339();
1427 let store = std::sync::Arc::new(InconsistentSnapshotStore {
1428 scanned_events: vec![
1429 StoredEvolutionEvent {
1430 seq: 1,
1431 timestamp: fresh_ts.clone(),
1432 prev_hash: String::new(),
1433 record_hash: "hash-1".into(),
1434 event: EvolutionEvent::GeneProjected {
1435 gene: scanned_gene.clone(),
1436 },
1437 },
1438 StoredEvolutionEvent {
1439 seq: 2,
1440 timestamp: fresh_ts,
1441 prev_hash: "hash-1".into(),
1442 record_hash: "hash-2".into(),
1443 event: EvolutionEvent::CapsuleCommitted {
1444 capsule: scanned_capsule.clone(),
1445 },
1446 },
1447 ],
1448 rebuilt_projection: EvolutionProjection {
1449 genes: vec![Gene {
1450 id: "gene-rebuilt".into(),
1451 signals: vec!["other".into()],
1452 strategy: vec!["b".into()],
1453 validation: vec!["oris-default".into()],
1454 state: AssetState::Promoted,
1455 }],
1456 ..Default::default()
1457 },
1458 });
1459 let selector = StoreBackedSelector::new(store);
1460 let input = SelectorInput {
1461 signals: vec!["signal".into()],
1462 env: scanned_capsule.env.clone(),
1463 spec_id: None,
1464 limit: 1,
1465 };
1466
1467 let candidates = selector.select(&input);
1468
1469 assert_eq!(candidates.len(), 1);
1470 assert_eq!(candidates[0].gene.id, scanned_gene.id);
1471 assert_eq!(candidates[0].capsules[0].id, scanned_capsule.id);
1472 }
1473
1474 #[test]
1475 fn selector_orders_results_stably() {
1476 let projection = EvolutionProjection {
1477 genes: vec![
1478 Gene {
1479 id: "gene-a".into(),
1480 signals: vec!["signal".into()],
1481 strategy: vec!["a".into()],
1482 validation: vec!["oris-default".into()],
1483 state: AssetState::Promoted,
1484 },
1485 Gene {
1486 id: "gene-b".into(),
1487 signals: vec!["signal".into()],
1488 strategy: vec!["b".into()],
1489 validation: vec!["oris-default".into()],
1490 state: AssetState::Promoted,
1491 },
1492 ],
1493 capsules: vec![
1494 Capsule {
1495 id: "capsule-a".into(),
1496 gene_id: "gene-a".into(),
1497 mutation_id: "m1".into(),
1498 run_id: "r1".into(),
1499 diff_hash: "1".into(),
1500 confidence: 0.7,
1501 env: EnvFingerprint {
1502 rustc_version: "rustc".into(),
1503 cargo_lock_hash: "lock".into(),
1504 target_triple: "x86_64-unknown-linux-gnu".into(),
1505 os: "linux".into(),
1506 },
1507 outcome: Outcome {
1508 success: true,
1509 validation_profile: "oris-default".into(),
1510 validation_duration_ms: 1,
1511 changed_files: vec!["crates/oris-kernel".into()],
1512 validator_hash: "v".into(),
1513 lines_changed: 1,
1514 replay_verified: false,
1515 },
1516 state: AssetState::Promoted,
1517 },
1518 Capsule {
1519 id: "capsule-b".into(),
1520 gene_id: "gene-b".into(),
1521 mutation_id: "m2".into(),
1522 run_id: "r2".into(),
1523 diff_hash: "2".into(),
1524 confidence: 0.7,
1525 env: EnvFingerprint {
1526 rustc_version: "rustc".into(),
1527 cargo_lock_hash: "lock".into(),
1528 target_triple: "x86_64-unknown-linux-gnu".into(),
1529 os: "linux".into(),
1530 },
1531 outcome: Outcome {
1532 success: true,
1533 validation_profile: "oris-default".into(),
1534 validation_duration_ms: 1,
1535 changed_files: vec!["crates/oris-kernel".into()],
1536 validator_hash: "v".into(),
1537 lines_changed: 1,
1538 replay_verified: false,
1539 },
1540 state: AssetState::Promoted,
1541 },
1542 ],
1543 reuse_counts: BTreeMap::from([("gene-a".into(), 3), ("gene-b".into(), 3)]),
1544 attempt_counts: BTreeMap::from([("gene-a".into(), 1), ("gene-b".into(), 1)]),
1545 last_updated_at: BTreeMap::from([
1546 ("gene-a".into(), Utc::now().to_rfc3339()),
1547 ("gene-b".into(), Utc::now().to_rfc3339()),
1548 ]),
1549 spec_ids_by_gene: BTreeMap::new(),
1550 };
1551 let selector = ProjectionSelector::new(projection);
1552 let input = SelectorInput {
1553 signals: vec!["signal".into()],
1554 env: EnvFingerprint {
1555 rustc_version: "rustc".into(),
1556 cargo_lock_hash: "lock".into(),
1557 target_triple: "x86_64-unknown-linux-gnu".into(),
1558 os: "linux".into(),
1559 },
1560 spec_id: None,
1561 limit: 2,
1562 };
1563 let first = selector.select(&input);
1564 let second = selector.select(&input);
1565 assert_eq!(first.len(), 2);
1566 assert_eq!(
1567 first
1568 .iter()
1569 .map(|candidate| candidate.gene.id.clone())
1570 .collect::<Vec<_>>(),
1571 second
1572 .iter()
1573 .map(|candidate| candidate.gene.id.clone())
1574 .collect::<Vec<_>>()
1575 );
1576 }
1577
1578 #[test]
1579 fn selector_can_narrow_by_spec_id() {
1580 let projection = EvolutionProjection {
1581 genes: vec![
1582 Gene {
1583 id: "gene-a".into(),
1584 signals: vec!["signal".into()],
1585 strategy: vec!["a".into()],
1586 validation: vec!["oris-default".into()],
1587 state: AssetState::Promoted,
1588 },
1589 Gene {
1590 id: "gene-b".into(),
1591 signals: vec!["signal".into()],
1592 strategy: vec!["b".into()],
1593 validation: vec!["oris-default".into()],
1594 state: AssetState::Promoted,
1595 },
1596 ],
1597 capsules: vec![
1598 Capsule {
1599 id: "capsule-a".into(),
1600 gene_id: "gene-a".into(),
1601 mutation_id: "m1".into(),
1602 run_id: "r1".into(),
1603 diff_hash: "1".into(),
1604 confidence: 0.7,
1605 env: EnvFingerprint {
1606 rustc_version: "rustc".into(),
1607 cargo_lock_hash: "lock".into(),
1608 target_triple: "x86_64-unknown-linux-gnu".into(),
1609 os: "linux".into(),
1610 },
1611 outcome: Outcome {
1612 success: true,
1613 validation_profile: "oris-default".into(),
1614 validation_duration_ms: 1,
1615 changed_files: vec!["crates/oris-kernel".into()],
1616 validator_hash: "v".into(),
1617 lines_changed: 1,
1618 replay_verified: false,
1619 },
1620 state: AssetState::Promoted,
1621 },
1622 Capsule {
1623 id: "capsule-b".into(),
1624 gene_id: "gene-b".into(),
1625 mutation_id: "m2".into(),
1626 run_id: "r2".into(),
1627 diff_hash: "2".into(),
1628 confidence: 0.7,
1629 env: EnvFingerprint {
1630 rustc_version: "rustc".into(),
1631 cargo_lock_hash: "lock".into(),
1632 target_triple: "x86_64-unknown-linux-gnu".into(),
1633 os: "linux".into(),
1634 },
1635 outcome: Outcome {
1636 success: true,
1637 validation_profile: "oris-default".into(),
1638 validation_duration_ms: 1,
1639 changed_files: vec!["crates/oris-kernel".into()],
1640 validator_hash: "v".into(),
1641 lines_changed: 1,
1642 replay_verified: false,
1643 },
1644 state: AssetState::Promoted,
1645 },
1646 ],
1647 reuse_counts: BTreeMap::from([("gene-a".into(), 3), ("gene-b".into(), 3)]),
1648 attempt_counts: BTreeMap::from([("gene-a".into(), 1), ("gene-b".into(), 1)]),
1649 last_updated_at: BTreeMap::from([
1650 ("gene-a".into(), Utc::now().to_rfc3339()),
1651 ("gene-b".into(), Utc::now().to_rfc3339()),
1652 ]),
1653 spec_ids_by_gene: BTreeMap::from([
1654 ("gene-a".into(), BTreeSet::from(["spec-a".to_string()])),
1655 ("gene-b".into(), BTreeSet::from(["spec-b".to_string()])),
1656 ]),
1657 };
1658 let selector = ProjectionSelector::new(projection);
1659 let input = SelectorInput {
1660 signals: vec!["signal".into()],
1661 env: EnvFingerprint {
1662 rustc_version: "rustc".into(),
1663 cargo_lock_hash: "lock".into(),
1664 target_triple: "x86_64-unknown-linux-gnu".into(),
1665 os: "linux".into(),
1666 },
1667 spec_id: Some("spec-b".into()),
1668 limit: 2,
1669 };
1670 let selected = selector.select(&input);
1671 assert_eq!(selected.len(), 1);
1672 assert_eq!(selected[0].gene.id, "gene-b");
1673 }
1674
1675 #[test]
1676 fn selector_prefers_closest_environment_match() {
1677 let projection = EvolutionProjection {
1678 genes: vec![
1679 Gene {
1680 id: "gene-a".into(),
1681 signals: vec!["signal".into()],
1682 strategy: vec!["a".into()],
1683 validation: vec!["oris-default".into()],
1684 state: AssetState::Promoted,
1685 },
1686 Gene {
1687 id: "gene-b".into(),
1688 signals: vec!["signal".into()],
1689 strategy: vec!["b".into()],
1690 validation: vec!["oris-default".into()],
1691 state: AssetState::Promoted,
1692 },
1693 ],
1694 capsules: vec![
1695 Capsule {
1696 id: "capsule-a-stale".into(),
1697 gene_id: "gene-a".into(),
1698 mutation_id: "m1".into(),
1699 run_id: "r1".into(),
1700 diff_hash: "1".into(),
1701 confidence: 0.2,
1702 env: EnvFingerprint {
1703 rustc_version: "old-rustc".into(),
1704 cargo_lock_hash: "other-lock".into(),
1705 target_triple: "aarch64-apple-darwin".into(),
1706 os: "macos".into(),
1707 },
1708 outcome: Outcome {
1709 success: true,
1710 validation_profile: "oris-default".into(),
1711 validation_duration_ms: 1,
1712 changed_files: vec!["crates/oris-kernel".into()],
1713 validator_hash: "v".into(),
1714 lines_changed: 1,
1715 replay_verified: false,
1716 },
1717 state: AssetState::Promoted,
1718 },
1719 Capsule {
1720 id: "capsule-a-best".into(),
1721 gene_id: "gene-a".into(),
1722 mutation_id: "m2".into(),
1723 run_id: "r2".into(),
1724 diff_hash: "2".into(),
1725 confidence: 0.9,
1726 env: EnvFingerprint {
1727 rustc_version: "rustc".into(),
1728 cargo_lock_hash: "lock".into(),
1729 target_triple: "x86_64-unknown-linux-gnu".into(),
1730 os: "linux".into(),
1731 },
1732 outcome: Outcome {
1733 success: true,
1734 validation_profile: "oris-default".into(),
1735 validation_duration_ms: 1,
1736 changed_files: vec!["crates/oris-kernel".into()],
1737 validator_hash: "v".into(),
1738 lines_changed: 1,
1739 replay_verified: false,
1740 },
1741 state: AssetState::Promoted,
1742 },
1743 Capsule {
1744 id: "capsule-b".into(),
1745 gene_id: "gene-b".into(),
1746 mutation_id: "m3".into(),
1747 run_id: "r3".into(),
1748 diff_hash: "3".into(),
1749 confidence: 0.7,
1750 env: EnvFingerprint {
1751 rustc_version: "rustc".into(),
1752 cargo_lock_hash: "different-lock".into(),
1753 target_triple: "x86_64-unknown-linux-gnu".into(),
1754 os: "linux".into(),
1755 },
1756 outcome: Outcome {
1757 success: true,
1758 validation_profile: "oris-default".into(),
1759 validation_duration_ms: 1,
1760 changed_files: vec!["crates/oris-kernel".into()],
1761 validator_hash: "v".into(),
1762 lines_changed: 1,
1763 replay_verified: false,
1764 },
1765 state: AssetState::Promoted,
1766 },
1767 ],
1768 reuse_counts: BTreeMap::from([("gene-a".into(), 3), ("gene-b".into(), 3)]),
1769 attempt_counts: BTreeMap::from([("gene-a".into(), 2), ("gene-b".into(), 1)]),
1770 last_updated_at: BTreeMap::from([
1771 ("gene-a".into(), Utc::now().to_rfc3339()),
1772 ("gene-b".into(), Utc::now().to_rfc3339()),
1773 ]),
1774 spec_ids_by_gene: BTreeMap::new(),
1775 };
1776 let selector = ProjectionSelector::new(projection);
1777 let input = SelectorInput {
1778 signals: vec!["signal".into()],
1779 env: EnvFingerprint {
1780 rustc_version: "rustc".into(),
1781 cargo_lock_hash: "lock".into(),
1782 target_triple: "x86_64-unknown-linux-gnu".into(),
1783 os: "linux".into(),
1784 },
1785 spec_id: None,
1786 limit: 2,
1787 };
1788
1789 let selected = selector.select(&input);
1790
1791 assert_eq!(selected.len(), 2);
1792 assert_eq!(selected[0].gene.id, "gene-a");
1793 assert_eq!(selected[0].capsules[0].id, "capsule-a-best");
1794 assert!(selected[0].score > selected[1].score);
1795 }
1796
1797 #[test]
1798 fn selector_preserves_fresh_candidate_scores_while_ranking_by_confidence() {
1799 let now = Utc::now();
1800 let projection = EvolutionProjection {
1801 genes: vec![Gene {
1802 id: "gene-fresh".into(),
1803 signals: vec!["missing".into()],
1804 strategy: vec!["a".into()],
1805 validation: vec!["oris-default".into()],
1806 state: AssetState::Promoted,
1807 }],
1808 capsules: vec![Capsule {
1809 id: "capsule-fresh".into(),
1810 gene_id: "gene-fresh".into(),
1811 mutation_id: "m1".into(),
1812 run_id: "r1".into(),
1813 diff_hash: "1".into(),
1814 confidence: 0.7,
1815 env: EnvFingerprint {
1816 rustc_version: "rustc".into(),
1817 cargo_lock_hash: "lock".into(),
1818 target_triple: "x86_64-unknown-linux-gnu".into(),
1819 os: "linux".into(),
1820 },
1821 outcome: Outcome {
1822 success: true,
1823 validation_profile: "oris-default".into(),
1824 validation_duration_ms: 1,
1825 changed_files: vec!["README.md".into()],
1826 validator_hash: "v".into(),
1827 lines_changed: 1,
1828 replay_verified: false,
1829 },
1830 state: AssetState::Promoted,
1831 }],
1832 reuse_counts: BTreeMap::from([("gene-fresh".into(), 1)]),
1833 attempt_counts: BTreeMap::from([("gene-fresh".into(), 1)]),
1834 last_updated_at: BTreeMap::from([("gene-fresh".into(), now.to_rfc3339())]),
1835 spec_ids_by_gene: BTreeMap::new(),
1836 };
1837 let selector = ProjectionSelector::with_now(projection, now);
1838 let input = SelectorInput {
1839 signals: vec![
1840 "missing".into(),
1841 "token-a".into(),
1842 "token-b".into(),
1843 "token-c".into(),
1844 ],
1845 env: EnvFingerprint {
1846 rustc_version: "rustc".into(),
1847 cargo_lock_hash: "lock".into(),
1848 target_triple: "x86_64-unknown-linux-gnu".into(),
1849 os: "linux".into(),
1850 },
1851 spec_id: None,
1852 limit: 1,
1853 };
1854
1855 let selected = selector.select(&input);
1856
1857 assert_eq!(selected.len(), 1);
1858 assert_eq!(selected[0].gene.id, "gene-fresh");
1859 assert!(selected[0].score > 0.35);
1860 }
1861
1862 #[test]
1863 fn selector_skips_stale_candidates_after_confidence_decay() {
1864 let now = Utc::now();
1865 let projection = EvolutionProjection {
1866 genes: vec![Gene {
1867 id: "gene-stale".into(),
1868 signals: vec!["missing readme".into()],
1869 strategy: vec!["a".into()],
1870 validation: vec!["oris-default".into()],
1871 state: AssetState::Promoted,
1872 }],
1873 capsules: vec![Capsule {
1874 id: "capsule-stale".into(),
1875 gene_id: "gene-stale".into(),
1876 mutation_id: "m1".into(),
1877 run_id: "r1".into(),
1878 diff_hash: "1".into(),
1879 confidence: 0.8,
1880 env: EnvFingerprint {
1881 rustc_version: "rustc".into(),
1882 cargo_lock_hash: "lock".into(),
1883 target_triple: "x86_64-unknown-linux-gnu".into(),
1884 os: "linux".into(),
1885 },
1886 outcome: Outcome {
1887 success: true,
1888 validation_profile: "oris-default".into(),
1889 validation_duration_ms: 1,
1890 changed_files: vec!["README.md".into()],
1891 validator_hash: "v".into(),
1892 lines_changed: 1,
1893 replay_verified: false,
1894 },
1895 state: AssetState::Promoted,
1896 }],
1897 reuse_counts: BTreeMap::from([("gene-stale".into(), 2)]),
1898 attempt_counts: BTreeMap::from([("gene-stale".into(), 1)]),
1899 last_updated_at: BTreeMap::from([(
1900 "gene-stale".into(),
1901 (now - chrono::Duration::hours(48)).to_rfc3339(),
1902 )]),
1903 spec_ids_by_gene: BTreeMap::new(),
1904 };
1905 let selector = ProjectionSelector::with_now(projection, now);
1906 let input = SelectorInput {
1907 signals: vec!["missing readme".into()],
1908 env: EnvFingerprint {
1909 rustc_version: "rustc".into(),
1910 cargo_lock_hash: "lock".into(),
1911 target_triple: "x86_64-unknown-linux-gnu".into(),
1912 os: "linux".into(),
1913 },
1914 spec_id: None,
1915 limit: 1,
1916 };
1917
1918 let selected = selector.select(&input);
1919
1920 assert!(selected.is_empty());
1921 assert!(decayed_replay_confidence(0.8, Some(48 * 60 * 60)) < MIN_REPLAY_CONFIDENCE);
1922 }
1923
1924 #[test]
1925 fn legacy_capsule_reused_events_deserialize_without_replay_run_id() {
1926 let serialized = r#"{
1927 "seq": 1,
1928 "timestamp": "2026-03-04T00:00:00Z",
1929 "prev_hash": "",
1930 "record_hash": "hash",
1931 "event": {
1932 "kind": "capsule_reused",
1933 "capsule_id": "capsule-1",
1934 "gene_id": "gene-1",
1935 "run_id": "run-1"
1936 }
1937}"#;
1938
1939 let stored = serde_json::from_str::<StoredEvolutionEvent>(serialized).unwrap();
1940
1941 match stored.event {
1942 EvolutionEvent::CapsuleReused {
1943 capsule_id,
1944 gene_id,
1945 run_id,
1946 replay_run_id,
1947 } => {
1948 assert_eq!(capsule_id, "capsule-1");
1949 assert_eq!(gene_id, "gene-1");
1950 assert_eq!(run_id, "run-1");
1951 assert_eq!(replay_run_id, None);
1952 }
1953 other => panic!("unexpected event: {other:?}"),
1954 }
1955 }
1956
1957 #[test]
1958 fn legacy_remote_asset_imported_events_deserialize_without_sender_id() {
1959 let serialized = r#"{
1960 "seq": 1,
1961 "timestamp": "2026-03-04T00:00:00Z",
1962 "prev_hash": "",
1963 "record_hash": "hash",
1964 "event": {
1965 "kind": "remote_asset_imported",
1966 "source": "Remote",
1967 "asset_ids": ["gene-1"]
1968 }
1969}"#;
1970
1971 let stored = serde_json::from_str::<StoredEvolutionEvent>(serialized).unwrap();
1972
1973 match stored.event {
1974 EvolutionEvent::RemoteAssetImported {
1975 source,
1976 asset_ids,
1977 sender_id,
1978 } => {
1979 assert_eq!(source, CandidateSource::Remote);
1980 assert_eq!(asset_ids, vec!["gene-1"]);
1981 assert_eq!(sender_id, None);
1982 }
1983 other => panic!("unexpected event: {other:?}"),
1984 }
1985 }
1986}