1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::f64::consts::E;
5use std::fs::{self, File, OpenOptions};
6use std::io::{BufRead, BufReader, Write};
7use std::path::{Path, PathBuf};
8use std::sync::Mutex;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use chrono::{DateTime, Utc};
12use oris_kernel::RunId;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use thiserror::Error;
16
17pub type MutationId = String;
18pub type GeneId = String;
19pub type CapsuleId = String;
20
21#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
22pub enum AssetState {
23 Candidate,
24 #[default]
25 Promoted,
26 Revoked,
27 Archived,
28 Quarantined,
29}
30
31#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
32pub enum CandidateSource {
33 #[default]
34 Local,
35 Remote,
36}
37
38#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
39pub struct BlastRadius {
40 pub files_changed: usize,
41 pub lines_changed: usize,
42}
43
44#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
45pub enum RiskLevel {
46 Low,
47 Medium,
48 High,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
52pub enum ArtifactEncoding {
53 UnifiedDiff,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
57pub enum MutationTarget {
58 WorkspaceRoot,
59 Crate { name: String },
60 Paths { allow: Vec<String> },
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
64pub struct MutationIntent {
65 pub id: MutationId,
66 pub intent: String,
67 pub target: MutationTarget,
68 pub expected_effect: String,
69 pub risk: RiskLevel,
70 pub signals: Vec<String>,
71 #[serde(default)]
72 pub spec_id: Option<String>,
73}
74
75#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
76pub struct MutationArtifact {
77 pub encoding: ArtifactEncoding,
78 pub payload: String,
79 pub base_revision: Option<String>,
80 pub content_hash: String,
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
84pub struct PreparedMutation {
85 pub intent: MutationIntent,
86 pub artifact: MutationArtifact,
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
90pub struct ValidationSnapshot {
91 pub success: bool,
92 pub profile: String,
93 pub duration_ms: u64,
94 pub summary: String,
95}
96
97#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
98pub struct Outcome {
99 pub success: bool,
100 pub validation_profile: String,
101 pub validation_duration_ms: u64,
102 pub changed_files: Vec<String>,
103 pub validator_hash: String,
104 #[serde(default)]
105 pub lines_changed: usize,
106 #[serde(default)]
107 pub replay_verified: bool,
108}
109
110#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
111pub struct EnvFingerprint {
112 pub rustc_version: String,
113 pub cargo_lock_hash: String,
114 pub target_triple: String,
115 pub os: String,
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
119pub struct Capsule {
120 pub id: CapsuleId,
121 pub gene_id: GeneId,
122 pub mutation_id: MutationId,
123 pub run_id: RunId,
124 pub diff_hash: String,
125 pub confidence: f32,
126 pub env: EnvFingerprint,
127 pub outcome: Outcome,
128 #[serde(default)]
129 pub state: AssetState,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
133pub struct Gene {
134 pub id: GeneId,
135 pub signals: Vec<String>,
136 pub strategy: Vec<String>,
137 pub validation: Vec<String>,
138 #[serde(default)]
139 pub state: AssetState,
140}
141
142#[derive(Clone, Debug, Serialize, Deserialize)]
143#[serde(tag = "kind", rename_all = "snake_case")]
144pub enum EvolutionEvent {
145 MutationDeclared {
146 mutation: PreparedMutation,
147 },
148 MutationApplied {
149 mutation_id: MutationId,
150 patch_hash: String,
151 changed_files: Vec<String>,
152 },
153 MutationRejected {
154 mutation_id: MutationId,
155 reason: String,
156 },
157 ValidationPassed {
158 mutation_id: MutationId,
159 report: ValidationSnapshot,
160 gene_id: Option<GeneId>,
161 },
162 ValidationFailed {
163 mutation_id: MutationId,
164 report: ValidationSnapshot,
165 gene_id: Option<GeneId>,
166 },
167 CapsuleCommitted {
168 capsule: Capsule,
169 },
170 CapsuleQuarantined {
171 capsule_id: CapsuleId,
172 },
173 CapsuleReleased {
174 capsule_id: CapsuleId,
175 state: AssetState,
176 },
177 CapsuleReused {
178 capsule_id: CapsuleId,
179 gene_id: GeneId,
180 run_id: RunId,
181 },
182 GeneProjected {
183 gene: Gene,
184 },
185 GenePromoted {
186 gene_id: GeneId,
187 },
188 GeneRevoked {
189 gene_id: GeneId,
190 reason: String,
191 },
192 GeneArchived {
193 gene_id: GeneId,
194 },
195 PromotionEvaluated {
196 gene_id: GeneId,
197 state: AssetState,
198 reason: String,
199 },
200 RemoteAssetImported {
201 source: CandidateSource,
202 asset_ids: Vec<String>,
203 },
204 SpecLinked {
205 mutation_id: MutationId,
206 spec_id: String,
207 },
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize)]
211pub struct StoredEvolutionEvent {
212 pub seq: u64,
213 pub timestamp: String,
214 pub prev_hash: String,
215 pub record_hash: String,
216 pub event: EvolutionEvent,
217}
218
219#[derive(Clone, Debug, Default, Serialize, Deserialize)]
220pub struct EvolutionProjection {
221 pub genes: Vec<Gene>,
222 pub capsules: Vec<Capsule>,
223 pub reuse_counts: BTreeMap<GeneId, u64>,
224 pub attempt_counts: BTreeMap<GeneId, u64>,
225 pub last_updated_at: BTreeMap<GeneId, String>,
226}
227
228#[derive(Clone, Debug)]
229pub struct SelectorInput {
230 pub signals: Vec<String>,
231 pub env: EnvFingerprint,
232 pub limit: usize,
233}
234
235#[derive(Clone, Debug)]
236pub struct GeneCandidate {
237 pub gene: Gene,
238 pub score: f32,
239 pub capsules: Vec<Capsule>,
240}
241
242pub trait Selector: Send + Sync {
243 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate>;
244}
245
246pub trait EvolutionStore: Send + Sync {
247 fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError>;
248 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError>;
249 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError>;
250}
251
252#[derive(Debug, Error)]
253pub enum EvolutionError {
254 #[error("I/O error: {0}")]
255 Io(String),
256 #[error("Serialization error: {0}")]
257 Serde(String),
258 #[error("Hash chain validation failed: {0}")]
259 HashChain(String),
260}
261
262pub struct JsonlEvolutionStore {
263 root_dir: PathBuf,
264 lock: Mutex<()>,
265}
266
267impl JsonlEvolutionStore {
268 pub fn new<P: Into<PathBuf>>(root_dir: P) -> Self {
269 Self {
270 root_dir: root_dir.into(),
271 lock: Mutex::new(()),
272 }
273 }
274
275 pub fn root_dir(&self) -> &Path {
276 &self.root_dir
277 }
278
279 fn ensure_layout(&self) -> Result<(), EvolutionError> {
280 fs::create_dir_all(&self.root_dir).map_err(io_err)?;
281 let lock_path = self.root_dir.join("LOCK");
282 if !lock_path.exists() {
283 File::create(lock_path).map_err(io_err)?;
284 }
285 let events_path = self.events_path();
286 if !events_path.exists() {
287 File::create(events_path).map_err(io_err)?;
288 }
289 Ok(())
290 }
291
292 fn events_path(&self) -> PathBuf {
293 self.root_dir.join("events.jsonl")
294 }
295
296 fn genes_path(&self) -> PathBuf {
297 self.root_dir.join("genes.json")
298 }
299
300 fn capsules_path(&self) -> PathBuf {
301 self.root_dir.join("capsules.json")
302 }
303
304 fn read_all_events(&self) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
305 self.ensure_layout()?;
306 let file = File::open(self.events_path()).map_err(io_err)?;
307 let reader = BufReader::new(file);
308 let mut events = Vec::new();
309 for line in reader.lines() {
310 let line = line.map_err(io_err)?;
311 if line.trim().is_empty() {
312 continue;
313 }
314 let event = serde_json::from_str::<StoredEvolutionEvent>(&line)
315 .map_err(|err| EvolutionError::Serde(err.to_string()))?;
316 events.push(event);
317 }
318 verify_hash_chain(&events)?;
319 Ok(events)
320 }
321
322 fn write_projection_files(
323 &self,
324 projection: &EvolutionProjection,
325 ) -> Result<(), EvolutionError> {
326 write_json_atomic(&self.genes_path(), &projection.genes)?;
327 write_json_atomic(&self.capsules_path(), &projection.capsules)?;
328 Ok(())
329 }
330
331 fn append_event_locked(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
332 let existing = self.read_all_events()?;
333 let next_seq = existing.last().map(|entry| entry.seq + 1).unwrap_or(1);
334 let prev_hash = existing
335 .last()
336 .map(|entry| entry.record_hash.clone())
337 .unwrap_or_default();
338 let timestamp = Utc::now().to_rfc3339();
339 let record_hash = hash_record(next_seq, ×tamp, &prev_hash, &event)?;
340 let stored = StoredEvolutionEvent {
341 seq: next_seq,
342 timestamp,
343 prev_hash,
344 record_hash,
345 event,
346 };
347 let mut file = OpenOptions::new()
348 .create(true)
349 .append(true)
350 .open(self.events_path())
351 .map_err(io_err)?;
352 let line =
353 serde_json::to_string(&stored).map_err(|err| EvolutionError::Serde(err.to_string()))?;
354 file.write_all(line.as_bytes()).map_err(io_err)?;
355 file.write_all(b"\n").map_err(io_err)?;
356 file.sync_data().map_err(io_err)?;
357
358 let events = self.read_all_events()?;
359 let projection = rebuild_projection_from_events(&events);
360 self.write_projection_files(&projection)?;
361 Ok(next_seq)
362 }
363}
364
365impl EvolutionStore for JsonlEvolutionStore {
366 fn append_event(&self, event: EvolutionEvent) -> Result<u64, EvolutionError> {
367 let _guard = self
368 .lock
369 .lock()
370 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
371 self.append_event_locked(event)
372 }
373
374 fn scan(&self, from_seq: u64) -> Result<Vec<StoredEvolutionEvent>, EvolutionError> {
375 let _guard = self
376 .lock
377 .lock()
378 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
379 Ok(self
380 .read_all_events()?
381 .into_iter()
382 .filter(|entry| entry.seq >= from_seq)
383 .collect())
384 }
385
386 fn rebuild_projection(&self) -> Result<EvolutionProjection, EvolutionError> {
387 let _guard = self
388 .lock
389 .lock()
390 .map_err(|_| EvolutionError::Io("evolution store lock poisoned".into()))?;
391 let projection = rebuild_projection_from_events(&self.read_all_events()?);
392 self.write_projection_files(&projection)?;
393 Ok(projection)
394 }
395}
396
397pub struct ProjectionSelector {
398 projection: EvolutionProjection,
399 now: DateTime<Utc>,
400}
401
402impl ProjectionSelector {
403 pub fn new(projection: EvolutionProjection) -> Self {
404 Self {
405 projection,
406 now: Utc::now(),
407 }
408 }
409
410 pub fn with_now(projection: EvolutionProjection, now: DateTime<Utc>) -> Self {
411 Self { projection, now }
412 }
413}
414
415impl Selector for ProjectionSelector {
416 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate> {
417 let mut out = Vec::new();
418 for gene in &self.projection.genes {
419 if gene.state != AssetState::Promoted {
420 continue;
421 }
422 let capsules = self
423 .projection
424 .capsules
425 .iter()
426 .filter(|capsule| {
427 capsule.gene_id == gene.id && capsule.state == AssetState::Promoted
428 })
429 .cloned()
430 .collect::<Vec<_>>();
431 if capsules.is_empty() {
432 continue;
433 }
434
435 let successful_capsules = capsules.len() as f64;
436 let attempts = self
437 .projection
438 .attempt_counts
439 .get(&gene.id)
440 .copied()
441 .unwrap_or(capsules.len() as u64) as f64;
442 let success_rate = if attempts == 0.0 {
443 0.0
444 } else {
445 successful_capsules / attempts
446 };
447 let successful_reuses = self
448 .projection
449 .reuse_counts
450 .get(&gene.id)
451 .copied()
452 .unwrap_or(0) as f64;
453 let reuse_count_factor = 1.0 + (1.0 + successful_reuses).ln();
454 let env_fingerprints = capsules
455 .iter()
456 .map(|capsule| fingerprint_key(&capsule.env))
457 .collect::<BTreeSet<_>>()
458 .len() as f64;
459 let env_diversity = (env_fingerprints / 5.0).min(1.0);
460 let signal_overlap = normalized_signal_overlap(&gene.signals, &input.signals);
461 let recency_decay = self
462 .projection
463 .last_updated_at
464 .get(&gene.id)
465 .and_then(|value| DateTime::parse_from_rfc3339(value).ok())
466 .map(|dt| {
467 let age_days = (self.now - dt.with_timezone(&Utc)).num_days().max(0) as f64;
468 E.powf(-age_days / 30.0)
469 })
470 .unwrap_or(0.0);
471 let score = (success_rate
472 * reuse_count_factor
473 * env_diversity
474 * recency_decay
475 * signal_overlap) as f32;
476 if score < 0.35 {
477 continue;
478 }
479 out.push(GeneCandidate {
480 gene: gene.clone(),
481 score,
482 capsules,
483 });
484 }
485
486 out.sort_by(|left, right| {
487 right
488 .score
489 .partial_cmp(&left.score)
490 .unwrap_or(std::cmp::Ordering::Equal)
491 .then_with(|| left.gene.id.cmp(&right.gene.id))
492 });
493 out.truncate(input.limit.max(1));
494 out
495 }
496}
497
498pub struct StoreBackedSelector {
499 store: std::sync::Arc<dyn EvolutionStore>,
500}
501
502impl StoreBackedSelector {
503 pub fn new(store: std::sync::Arc<dyn EvolutionStore>) -> Self {
504 Self { store }
505 }
506}
507
508impl Selector for StoreBackedSelector {
509 fn select(&self, input: &SelectorInput) -> Vec<GeneCandidate> {
510 match self.store.rebuild_projection() {
511 Ok(projection) => ProjectionSelector::new(projection).select(input),
512 Err(_) => Vec::new(),
513 }
514 }
515}
516
517pub fn rebuild_projection_from_events(events: &[StoredEvolutionEvent]) -> EvolutionProjection {
518 let mut genes = BTreeMap::<GeneId, Gene>::new();
519 let mut capsules = BTreeMap::<CapsuleId, Capsule>::new();
520 let mut reuse_counts = BTreeMap::<GeneId, u64>::new();
521 let mut attempt_counts = BTreeMap::<GeneId, u64>::new();
522 let mut last_updated_at = BTreeMap::<GeneId, String>::new();
523 let mut mutation_to_gene = HashMap::<MutationId, GeneId>::new();
524
525 for stored in events {
526 match &stored.event {
527 EvolutionEvent::GeneProjected { gene } => {
528 genes.insert(gene.id.clone(), gene.clone());
529 last_updated_at.insert(gene.id.clone(), stored.timestamp.clone());
530 }
531 EvolutionEvent::GenePromoted { gene_id } => {
532 if let Some(gene) = genes.get_mut(gene_id) {
533 gene.state = AssetState::Promoted;
534 }
535 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
536 }
537 EvolutionEvent::GeneRevoked { gene_id, .. } => {
538 if let Some(gene) = genes.get_mut(gene_id) {
539 gene.state = AssetState::Revoked;
540 }
541 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
542 }
543 EvolutionEvent::GeneArchived { gene_id } => {
544 if let Some(gene) = genes.get_mut(gene_id) {
545 gene.state = AssetState::Archived;
546 }
547 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
548 }
549 EvolutionEvent::PromotionEvaluated { gene_id, state, .. } => {
550 if let Some(gene) = genes.get_mut(gene_id) {
551 gene.state = state.clone();
552 }
553 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
554 }
555 EvolutionEvent::CapsuleCommitted { capsule } => {
556 mutation_to_gene.insert(capsule.mutation_id.clone(), capsule.gene_id.clone());
557 capsules.insert(capsule.id.clone(), capsule.clone());
558 *attempt_counts.entry(capsule.gene_id.clone()).or_insert(0) += 1;
559 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
560 }
561 EvolutionEvent::CapsuleQuarantined { capsule_id } => {
562 if let Some(capsule) = capsules.get_mut(capsule_id) {
563 capsule.state = AssetState::Quarantined;
564 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
565 }
566 }
567 EvolutionEvent::CapsuleReleased { capsule_id, state } => {
568 if let Some(capsule) = capsules.get_mut(capsule_id) {
569 capsule.state = state.clone();
570 last_updated_at.insert(capsule.gene_id.clone(), stored.timestamp.clone());
571 }
572 }
573 EvolutionEvent::CapsuleReused { gene_id, .. } => {
574 *reuse_counts.entry(gene_id.clone()).or_insert(0) += 1;
575 last_updated_at.insert(gene_id.clone(), stored.timestamp.clone());
576 }
577 EvolutionEvent::ValidationFailed {
578 mutation_id,
579 gene_id,
580 ..
581 } => {
582 let id = gene_id
583 .clone()
584 .or_else(|| mutation_to_gene.get(mutation_id).cloned());
585 if let Some(gene_id) = id {
586 *attempt_counts.entry(gene_id.clone()).or_insert(0) += 1;
587 last_updated_at.insert(gene_id, stored.timestamp.clone());
588 }
589 }
590 EvolutionEvent::ValidationPassed {
591 mutation_id,
592 gene_id,
593 ..
594 } => {
595 let id = gene_id
596 .clone()
597 .or_else(|| mutation_to_gene.get(mutation_id).cloned());
598 if let Some(gene_id) = id {
599 *attempt_counts.entry(gene_id.clone()).or_insert(0) += 1;
600 last_updated_at.insert(gene_id, stored.timestamp.clone());
601 }
602 }
603 _ => {}
604 }
605 }
606
607 EvolutionProjection {
608 genes: genes.into_values().collect(),
609 capsules: capsules.into_values().collect(),
610 reuse_counts,
611 attempt_counts,
612 last_updated_at,
613 }
614}
615
616pub fn default_store_root() -> PathBuf {
617 PathBuf::from(".oris").join("evolution")
618}
619
620pub fn hash_string(input: &str) -> String {
621 let mut hasher = Sha256::new();
622 hasher.update(input.as_bytes());
623 hex::encode(hasher.finalize())
624}
625
626pub fn stable_hash_json<T: Serialize>(value: &T) -> Result<String, EvolutionError> {
627 let bytes = serde_json::to_vec(value).map_err(|err| EvolutionError::Serde(err.to_string()))?;
628 let mut hasher = Sha256::new();
629 hasher.update(bytes);
630 Ok(hex::encode(hasher.finalize()))
631}
632
633pub fn compute_artifact_hash(payload: &str) -> String {
634 hash_string(payload)
635}
636
637pub fn next_id(prefix: &str) -> String {
638 let nanos = SystemTime::now()
639 .duration_since(UNIX_EPOCH)
640 .unwrap_or_default()
641 .as_nanos();
642 format!("{prefix}-{nanos:x}")
643}
644
645fn normalized_signal_overlap(gene_signals: &[String], input_signals: &[String]) -> f64 {
646 if input_signals.is_empty() {
647 return 0.0;
648 }
649 let gene = gene_signals
650 .iter()
651 .map(|signal| signal.to_ascii_lowercase())
652 .collect::<BTreeSet<_>>();
653 let input = input_signals
654 .iter()
655 .map(|signal| signal.to_ascii_lowercase())
656 .collect::<BTreeSet<_>>();
657 let matched = input.iter().filter(|signal| gene.contains(*signal)).count() as f64;
658 matched / input.len() as f64
659}
660
661fn fingerprint_key(env: &EnvFingerprint) -> String {
662 format!(
663 "{}|{}|{}|{}",
664 env.rustc_version, env.cargo_lock_hash, env.target_triple, env.os
665 )
666}
667
668fn hash_record(
669 seq: u64,
670 timestamp: &str,
671 prev_hash: &str,
672 event: &EvolutionEvent,
673) -> Result<String, EvolutionError> {
674 stable_hash_json(&(seq, timestamp, prev_hash, event))
675}
676
677fn verify_hash_chain(events: &[StoredEvolutionEvent]) -> Result<(), EvolutionError> {
678 let mut previous_hash = String::new();
679 let mut expected_seq = 1u64;
680 for event in events {
681 if event.seq != expected_seq {
682 return Err(EvolutionError::HashChain(format!(
683 "expected seq {}, found {}",
684 expected_seq, event.seq
685 )));
686 }
687 if event.prev_hash != previous_hash {
688 return Err(EvolutionError::HashChain(format!(
689 "event {} prev_hash mismatch",
690 event.seq
691 )));
692 }
693 let actual_hash = hash_record(event.seq, &event.timestamp, &event.prev_hash, &event.event)?;
694 if actual_hash != event.record_hash {
695 return Err(EvolutionError::HashChain(format!(
696 "event {} record_hash mismatch",
697 event.seq
698 )));
699 }
700 previous_hash = event.record_hash.clone();
701 expected_seq += 1;
702 }
703 Ok(())
704}
705
706fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> Result<(), EvolutionError> {
707 let tmp_path = path.with_extension("tmp");
708 let bytes =
709 serde_json::to_vec_pretty(value).map_err(|err| EvolutionError::Serde(err.to_string()))?;
710 fs::write(&tmp_path, bytes).map_err(io_err)?;
711 fs::rename(&tmp_path, path).map_err(io_err)?;
712 Ok(())
713}
714
715fn io_err(err: std::io::Error) -> EvolutionError {
716 EvolutionError::Io(err.to_string())
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 fn temp_root(name: &str) -> PathBuf {
724 std::env::temp_dir().join(format!("oris-evolution-{name}-{}", next_id("t")))
725 }
726
727 fn sample_mutation() -> PreparedMutation {
728 PreparedMutation {
729 intent: MutationIntent {
730 id: "mutation-1".into(),
731 intent: "tighten borrow scope".into(),
732 target: MutationTarget::Paths {
733 allow: vec!["crates/oris-kernel".into()],
734 },
735 expected_effect: "cargo check passes".into(),
736 risk: RiskLevel::Low,
737 signals: vec!["rust borrow error".into()],
738 spec_id: None,
739 },
740 artifact: MutationArtifact {
741 encoding: ArtifactEncoding::UnifiedDiff,
742 payload: "diff --git a/foo b/foo".into(),
743 base_revision: Some("HEAD".into()),
744 content_hash: compute_artifact_hash("diff --git a/foo b/foo"),
745 },
746 }
747 }
748
749 #[test]
750 fn append_event_assigns_monotonic_seq() {
751 let root = temp_root("seq");
752 let store = JsonlEvolutionStore::new(root);
753 let first = store
754 .append_event(EvolutionEvent::MutationDeclared {
755 mutation: sample_mutation(),
756 })
757 .unwrap();
758 let second = store
759 .append_event(EvolutionEvent::MutationRejected {
760 mutation_id: "mutation-1".into(),
761 reason: "no-op".into(),
762 })
763 .unwrap();
764 assert_eq!(first, 1);
765 assert_eq!(second, 2);
766 }
767
768 #[test]
769 fn tampered_hash_chain_is_rejected() {
770 let root = temp_root("tamper");
771 let store = JsonlEvolutionStore::new(&root);
772 store
773 .append_event(EvolutionEvent::MutationDeclared {
774 mutation: sample_mutation(),
775 })
776 .unwrap();
777 let path = root.join("events.jsonl");
778 let contents = fs::read_to_string(&path).unwrap();
779 let mutated = contents.replace("tighten borrow scope", "tampered");
780 fs::write(&path, mutated).unwrap();
781 let result = store.scan(1);
782 assert!(matches!(result, Err(EvolutionError::HashChain(_))));
783 }
784
785 #[test]
786 fn rebuild_projection_after_cache_deletion() {
787 let root = temp_root("projection");
788 let store = JsonlEvolutionStore::new(&root);
789 let gene = Gene {
790 id: "gene-1".into(),
791 signals: vec!["rust borrow error".into()],
792 strategy: vec!["crates".into()],
793 validation: vec!["oris-default".into()],
794 state: AssetState::Promoted,
795 };
796 let capsule = Capsule {
797 id: "capsule-1".into(),
798 gene_id: gene.id.clone(),
799 mutation_id: "mutation-1".into(),
800 run_id: "run-1".into(),
801 diff_hash: "abc".into(),
802 confidence: 0.7,
803 env: EnvFingerprint {
804 rustc_version: "rustc 1.80".into(),
805 cargo_lock_hash: "lock".into(),
806 target_triple: "x86_64-unknown-linux-gnu".into(),
807 os: "linux".into(),
808 },
809 outcome: Outcome {
810 success: true,
811 validation_profile: "oris-default".into(),
812 validation_duration_ms: 100,
813 changed_files: vec!["crates/oris-kernel/src/lib.rs".into()],
814 validator_hash: "vh".into(),
815 lines_changed: 1,
816 replay_verified: false,
817 },
818 state: AssetState::Promoted,
819 };
820 store
821 .append_event(EvolutionEvent::GeneProjected { gene })
822 .unwrap();
823 store
824 .append_event(EvolutionEvent::CapsuleCommitted { capsule })
825 .unwrap();
826 fs::remove_file(root.join("genes.json")).unwrap();
827 fs::remove_file(root.join("capsules.json")).unwrap();
828 let projection = store.rebuild_projection().unwrap();
829 assert_eq!(projection.genes.len(), 1);
830 assert_eq!(projection.capsules.len(), 1);
831 }
832
833 #[test]
834 fn selector_orders_results_stably() {
835 let projection = EvolutionProjection {
836 genes: vec![
837 Gene {
838 id: "gene-a".into(),
839 signals: vec!["signal".into()],
840 strategy: vec!["a".into()],
841 validation: vec!["oris-default".into()],
842 state: AssetState::Promoted,
843 },
844 Gene {
845 id: "gene-b".into(),
846 signals: vec!["signal".into()],
847 strategy: vec!["b".into()],
848 validation: vec!["oris-default".into()],
849 state: AssetState::Promoted,
850 },
851 ],
852 capsules: vec![
853 Capsule {
854 id: "capsule-a".into(),
855 gene_id: "gene-a".into(),
856 mutation_id: "m1".into(),
857 run_id: "r1".into(),
858 diff_hash: "1".into(),
859 confidence: 0.7,
860 env: EnvFingerprint {
861 rustc_version: "rustc".into(),
862 cargo_lock_hash: "lock".into(),
863 target_triple: "x86_64-unknown-linux-gnu".into(),
864 os: "linux".into(),
865 },
866 outcome: Outcome {
867 success: true,
868 validation_profile: "oris-default".into(),
869 validation_duration_ms: 1,
870 changed_files: vec!["crates/oris-kernel".into()],
871 validator_hash: "v".into(),
872 lines_changed: 1,
873 replay_verified: false,
874 },
875 state: AssetState::Promoted,
876 },
877 Capsule {
878 id: "capsule-b".into(),
879 gene_id: "gene-b".into(),
880 mutation_id: "m2".into(),
881 run_id: "r2".into(),
882 diff_hash: "2".into(),
883 confidence: 0.7,
884 env: EnvFingerprint {
885 rustc_version: "rustc".into(),
886 cargo_lock_hash: "lock".into(),
887 target_triple: "x86_64-unknown-linux-gnu".into(),
888 os: "linux".into(),
889 },
890 outcome: Outcome {
891 success: true,
892 validation_profile: "oris-default".into(),
893 validation_duration_ms: 1,
894 changed_files: vec!["crates/oris-kernel".into()],
895 validator_hash: "v".into(),
896 lines_changed: 1,
897 replay_verified: false,
898 },
899 state: AssetState::Promoted,
900 },
901 ],
902 reuse_counts: BTreeMap::from([("gene-a".into(), 3), ("gene-b".into(), 3)]),
903 attempt_counts: BTreeMap::from([("gene-a".into(), 1), ("gene-b".into(), 1)]),
904 last_updated_at: BTreeMap::from([
905 ("gene-a".into(), Utc::now().to_rfc3339()),
906 ("gene-b".into(), Utc::now().to_rfc3339()),
907 ]),
908 };
909 let selector = ProjectionSelector::new(projection);
910 let input = SelectorInput {
911 signals: vec!["signal".into()],
912 env: EnvFingerprint {
913 rustc_version: "rustc".into(),
914 cargo_lock_hash: "lock".into(),
915 target_triple: "x86_64-unknown-linux-gnu".into(),
916 os: "linux".into(),
917 },
918 limit: 2,
919 };
920 let first = selector.select(&input);
921 let second = selector.select(&input);
922 assert_eq!(first.len(), 2);
923 assert_eq!(
924 first
925 .iter()
926 .map(|candidate| candidate.gene.id.clone())
927 .collect::<Vec<_>>(),
928 second
929 .iter()
930 .map(|candidate| candidate.gene.id.clone())
931 .collect::<Vec<_>>()
932 );
933 }
934}