mur_common/skill/
credit.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum CreditKind {
13 Author,
14 Mutator,
15 Recombiner,
16 Propagator,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum CreditEvidence {
22 Author,
23 Mutator {
24 from_version: String,
25 diff_summary: String,
26 },
27 Recombiner {
28 role: String,
29 child: String,
30 },
31 Propagator {
32 from_agent: String,
33 fitness_at_install: f64,
34 samples_at_install: u64,
35 },
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct CreditEntry {
40 pub ts: DateTime<Utc>,
41 pub skill: String,
42 pub skill_version: String,
43 pub kind: CreditKind,
44 pub agent: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub evidence: Option<CreditEvidence>,
49 pub source: String,
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn round_trip_author_entry() {
59 let entry = CreditEntry {
60 ts: DateTime::parse_from_rfc3339("2026-05-27T10:21:33Z")
61 .unwrap()
62 .with_timezone(&Utc),
63 skill: "research-prices".into(),
64 skill_version: "1.0.0".into(),
65 kind: CreditKind::Author,
66 agent: "alice".into(),
67 evidence: None,
68 source: "human:alice".into(),
69 };
70 let json = serde_json::to_string(&entry).unwrap();
71 let back: CreditEntry = serde_json::from_str(&json).unwrap();
72 assert_eq!(entry, back);
73 }
74
75 #[test]
76 fn propagator_evidence_round_trips() {
77 let entry = CreditEntry {
78 ts: Utc::now(),
79 skill: "x".into(),
80 skill_version: "1.0.0".into(),
81 kind: CreditKind::Propagator,
82 agent: "bob".into(),
83 evidence: Some(CreditEvidence::Propagator {
84 from_agent: "alice".into(),
85 fitness_at_install: 0.78,
86 samples_at_install: 7,
87 }),
88 source: "agent://alice".into(),
89 };
90 let json = serde_json::to_string(&entry).unwrap();
91 let back: CreditEntry = serde_json::from_str(&json).unwrap();
92 assert_eq!(entry, back);
93 }
94
95 #[test]
96 fn unknown_kind_returns_error() {
97 let raw = r#"{"ts":"2026-05-27T10:21:33Z","skill":"x","skill_version":"1.0.0","kind":"future_kind","agent":"alice","source":"human:alice"}"#;
98 let result: Result<CreditEntry, _> = serde_json::from_str(raw);
99 assert!(
100 result.is_err(),
101 "unknown kind should fail to deserialize; reader code must filter at the line level"
102 );
103 }
104}