1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct MechanismBundleV1 {
12 pub mechanism_id: String,
13 pub name: String,
14 pub description: String,
15 pub fit_score: f64,
16 pub refuter_suite: Option<RefuterSuiteV1>,
17 pub status: MechanismStatus,
18 pub version: u32,
19 pub superseded_by: Option<String>,
20 pub timestamp: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct RefuterSuiteV1 {
26 pub suite_id: String,
27 pub refuters: Vec<RefuterV1>,
28 pub all_passed: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RefuterV1 {
34 pub refuter_id: String,
35 pub refutation_method: String,
36 pub result: RefutationResult,
37 pub evidence: String,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum RefutationResult {
43 Survived,
45 Refuted,
47 Inconclusive,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub enum MechanismStatus {
54 Candidate,
56 Tested,
58 Refuted,
60 Superseded,
62 Published,
64}
65
66pub struct MechanismAdapter;
68
69impl MechanismAdapter {
70 pub fn fit_mechanism(name: &str, description: &str, fit_score: f64) -> MechanismBundleV1 {
72 MechanismBundleV1 {
73 mechanism_id: format!("mech:{}:{}", name, chrono::Utc::now().timestamp()),
74 name: name.to_string(),
75 description: description.to_string(),
76 fit_score,
77 refuter_suite: None,
78 status: MechanismStatus::Candidate,
79 version: 1,
80 superseded_by: None,
81 timestamp: chrono::Utc::now().to_rfc3339(),
82 }
83 }
84
85 pub fn refute_mechanism(mechanism: &mut MechanismBundleV1, refuters: Vec<RefuterV1>) {
87 let all_passed = refuters
88 .iter()
89 .all(|r| r.result == RefutationResult::Survived);
90 let any_refuted = refuters
91 .iter()
92 .any(|r| r.result == RefutationResult::Refuted);
93
94 mechanism.refuter_suite = Some(RefuterSuiteV1 {
95 suite_id: format!(
96 "rs:{}:{}",
97 mechanism.mechanism_id,
98 chrono::Utc::now().timestamp()
99 ),
100 refuters,
101 all_passed,
102 });
103
104 mechanism.status = if any_refuted {
105 MechanismStatus::Refuted
106 } else if all_passed {
107 MechanismStatus::Tested
108 } else {
109 MechanismStatus::Candidate
110 };
111 }
112
113 pub fn supersede_mechanism(
115 old: &mut MechanismBundleV1,
116 new_name: &str,
117 new_description: &str,
118 new_fit_score: f64,
119 ) -> MechanismBundleV1 {
120 old.status = MechanismStatus::Superseded;
121 let new_version = old.version + 1;
122 let new_id = format!("mech:{}:{}", new_name, chrono::Utc::now().timestamp());
123 old.superseded_by = Some(new_id.clone());
124
125 MechanismBundleV1 {
126 mechanism_id: new_id,
127 name: new_name.to_string(),
128 description: new_description.to_string(),
129 fit_score: new_fit_score,
130 refuter_suite: None,
131 status: MechanismStatus::Candidate,
132 version: new_version,
133 superseded_by: None,
134 timestamp: chrono::Utc::now().to_rfc3339(),
135 }
136 }
137
138 pub fn can_promote(mechanism: &MechanismBundleV1) -> bool {
140 match &mechanism.refuter_suite {
142 Some(suite) => suite.all_passed && mechanism.status == MechanismStatus::Tested,
143 None => false,
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn fit_mechanism_starts_as_candidate() {
154 let m = MechanismAdapter::fit_mechanism("test-mech", "a test", 0.85);
155 assert_eq!(m.status, MechanismStatus::Candidate);
156 assert!(m.refuter_suite.is_none());
157 }
158
159 #[test]
160 fn mechanism_fit_never_promotes_without_refuter_suite() {
161 let m = MechanismAdapter::fit_mechanism("test-mech", "a test", 0.99);
162 assert!(!MechanismAdapter::can_promote(&m));
163 }
164
165 #[test]
166 fn refuted_mechanism_cannot_promote() {
167 let mut m = MechanismAdapter::fit_mechanism("test-mech", "a test", 0.85);
168 MechanismAdapter::refute_mechanism(
169 &mut m,
170 vec![RefuterV1 {
171 refuter_id: "r1".into(),
172 refutation_method: "counterexample".into(),
173 result: RefutationResult::Refuted,
174 evidence: "found counterexample".into(),
175 }],
176 );
177 assert_eq!(m.status, MechanismStatus::Refuted);
178 assert!(!MechanismAdapter::can_promote(&m));
179 }
180
181 #[test]
182 fn survived_mechanism_can_promote() {
183 let mut m = MechanismAdapter::fit_mechanism("test-mech", "a test", 0.85);
184 MechanismAdapter::refute_mechanism(
185 &mut m,
186 vec![RefuterV1 {
187 refuter_id: "r1".into(),
188 refutation_method: "counterexample".into(),
189 result: RefutationResult::Survived,
190 evidence: "no counterexample found".into(),
191 }],
192 );
193 assert_eq!(m.status, MechanismStatus::Tested);
194 assert!(MechanismAdapter::can_promote(&m));
195 }
196
197 #[test]
198 fn supersede_creates_new_version() {
199 let mut old = MechanismAdapter::fit_mechanism("mech-v1", "first", 0.7);
200 let new = MechanismAdapter::supersede_mechanism(&mut old, "mech-v2", "second", 0.9);
201 assert_eq!(old.status, MechanismStatus::Superseded);
202 assert_eq!(new.version, 2);
203 assert_eq!(
204 old.superseded_by.as_deref(),
205 Some(new.mechanism_id.as_str())
206 );
207 }
208
209 #[test]
210 fn inconclusive_refuter_keeps_candidate() {
211 let mut m = MechanismAdapter::fit_mechanism("test", "test", 0.5);
212 MechanismAdapter::refute_mechanism(
213 &mut m,
214 vec![RefuterV1 {
215 refuter_id: "r1".into(),
216 refutation_method: "fuzz".into(),
217 result: RefutationResult::Inconclusive,
218 evidence: "need more data".into(),
219 }],
220 );
221 assert_eq!(m.status, MechanismStatus::Candidate);
222 assert!(!MechanismAdapter::can_promote(&m));
223 }
224}