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