mur_common/skill/
evolution.rs1use serde::{Deserialize, Serialize};
4
5pub const CURRENT_GENERATION: u32 = 0;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct EvolutionEvent {
9 pub version: String,
10 #[serde(default)]
11 pub generation: u32,
12 pub source: String,
13 pub changes: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub quality_score: Option<f64>,
16 #[serde(default)]
17 pub timestamp: String,
18}
19
20impl EvolutionEvent {
21 pub fn initial_human(publisher: &str, version: &str) -> Self {
22 Self {
23 version: version.to_string(),
24 generation: 0,
25 source: format!("human:{publisher}"),
26 changes: "Initial creation".into(),
27 quality_score: None,
28 timestamp: chrono::Utc::now().to_rfc3339(),
29 }
30 }
31
32 pub fn evolved(version: &str, generation: u32, changes: &str, score: f64) -> Self {
33 Self {
34 version: version.to_string(),
35 generation,
36 source: "agent:evolver".into(),
37 changes: changes.into(),
38 quality_score: Some(score),
39 timestamp: chrono::Utc::now().to_rfc3339(),
40 }
41 }
42
43 pub fn recombined(
46 version: &str,
47 generation: u32,
48 parent_a: &str,
49 parent_b: &str,
50 strategy: &str,
51 output_skill: &str,
52 ) -> Self {
53 Self {
54 version: version.to_string(),
55 generation,
56 source: "agent:recombiner".into(),
57 changes: format!(
58 "recombine: a={parent_a}, b={parent_b}, strategy={strategy}, output={output_skill}"
59 ),
60 quality_score: None,
61 timestamp: chrono::Utc::now().to_rfc3339(),
62 }
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn initial_human_produces_correct_shape() {
72 let event = EvolutionEvent::initial_human("david", "0.1.0");
73 assert_eq!(event.version, "0.1.0");
74 assert_eq!(event.generation, 0);
75 assert_eq!(event.source, "human:david");
76 assert_eq!(event.changes, "Initial creation");
77 assert!(event.quality_score.is_none());
78 assert!(!event.timestamp.is_empty());
79 }
80
81 #[test]
82 fn evolved_bumps_generation_and_sets_quality_score() {
83 let event = EvolutionEvent::evolved("0.1.1", 1, "fixed tool name", 0.85);
84 assert_eq!(event.version, "0.1.1");
85 assert_eq!(event.generation, 1);
86 assert_eq!(event.source, "agent:evolver");
87 assert_eq!(event.changes, "fixed tool name");
88 assert_eq!(event.quality_score, Some(0.85));
89 }
90
91 #[test]
92 fn recombined_sets_recombiner_source_and_packs_metadata() {
93 let event = EvolutionEvent::recombined(
94 "0.1.0",
95 5,
96 "local/research-prices",
97 "agent://bob/lookup",
98 "union",
99 "combined-research",
100 );
101 assert_eq!(event.source, "agent:recombiner");
102 assert_eq!(event.version, "0.1.0");
103 assert_eq!(event.generation, 5);
104 assert!(event.changes.starts_with("recombine: "));
105 assert!(event.changes.contains("a=local/research-prices"));
106 assert!(event.changes.contains("b=agent://bob/lookup"));
107 assert!(event.changes.contains("strategy=union"));
108 assert!(event.changes.contains("output=combined-research"));
109 assert!(event.quality_score.is_none());
110 assert!(!event.timestamp.is_empty());
111 }
112
113 #[test]
114 fn evolution_event_roundtrips() {
115 let event = EvolutionEvent::evolved("1.2.3", 3, "patched X", 0.92);
116 let json = serde_json::to_string(&event).unwrap();
117 let back: EvolutionEvent = serde_json::from_str(&json).unwrap();
118 assert_eq!(back.version, event.version);
119 assert_eq!(back.generation, event.generation);
120 assert_eq!(back.source, event.source);
121 assert_eq!(back.changes, event.changes);
122 assert_eq!(back.quality_score, event.quality_score);
123 }
124}