Skip to main content

mur_common/skill/
evolution.rs

1//! Skill evolution log — records every generation of a skill.
2
3use 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    /// Constructor for M7b recombination events. `parent_a` and `parent_b`
44    /// are stringified `SkillRef`s (e.g. `local/foo` or `agent://bob/bar`).
45    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}