1use serde::{Deserialize, Serialize};
2
3use crate::error::ParseEnumError;
4use crate::ids::{BranchId, NodeId};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub struct KnowledgeNode {
14 pub id: NodeId,
15 pub branch_id: BranchId,
16 pub nature: KnowledgeNature,
17 pub weight: KnowledgeWeight,
18 pub confidence: f64,
19 pub adoption_count: u32,
20 pub total_count: u32,
21 pub description: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
25 pub ext_data: Option<serde_json::Value>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum KnowledgeNature {
32 Fact,
34 Convention,
36 Observation,
38 Decision,
40 Preference,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Trend {
52 Rising,
54 Stable,
56 Declining,
58 Unknown,
60}
61
62impl Trend {
63 pub fn as_str(&self) -> &'static str {
65 match self {
66 Self::Rising => "rising",
67 Self::Stable => "stable",
68 Self::Declining => "declining",
69 Self::Unknown => "unknown",
70 }
71 }
72}
73
74impl std::fmt::Display for Trend {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Rising => write!(f, "Rising"),
78 Self::Stable => write!(f, "Stable"),
79 Self::Declining => write!(f, "Declining"),
80 Self::Unknown => write!(f, "Unknown"),
81 }
82 }
83}
84
85impl std::str::FromStr for Trend {
86 type Err = ParseEnumError;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 match s {
90 "rising" => Ok(Self::Rising),
91 "stable" => Ok(Self::Stable),
92 "declining" => Ok(Self::Declining),
93 "unknown" => Ok(Self::Unknown),
94 _ => Err(ParseEnumError {
95 type_name: "Trend",
96 value: s.to_owned(),
97 }),
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum KnowledgeWeight {
106 Rule,
108 Strong,
110 Moderate,
112 Weak,
114 Info,
116}
117
118impl KnowledgeNature {
119 pub fn as_str(&self) -> &'static str {
121 match self {
122 Self::Fact => "fact",
123 Self::Convention => "convention",
124 Self::Observation => "observation",
125 Self::Decision => "decision",
126 Self::Preference => "preference",
127 }
128 }
129}
130
131impl std::fmt::Display for KnowledgeNature {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Self::Fact => write!(f, "Fact"),
135 Self::Convention => write!(f, "Convention"),
136 Self::Observation => write!(f, "Observation"),
137 Self::Decision => write!(f, "Decision"),
138 Self::Preference => write!(f, "Preference"),
139 }
140 }
141}
142
143impl std::str::FromStr for KnowledgeNature {
144 type Err = ParseEnumError;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 match s {
148 "fact" => Ok(Self::Fact),
149 "convention" => Ok(Self::Convention),
150 "observation" => Ok(Self::Observation),
151 "decision" => Ok(Self::Decision),
152 "preference" => Ok(Self::Preference),
153 _ => Err(ParseEnumError {
154 type_name: "KnowledgeNature",
155 value: s.to_owned(),
156 }),
157 }
158 }
159}
160
161impl KnowledgeWeight {
162 pub fn as_str(&self) -> &'static str {
164 match self {
165 Self::Rule => "rule",
166 Self::Strong => "strong",
167 Self::Moderate => "moderate",
168 Self::Weak => "weak",
169 Self::Info => "info",
170 }
171 }
172}
173
174impl std::fmt::Display for KnowledgeWeight {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 Self::Rule => write!(f, "Rule"),
178 Self::Strong => write!(f, "Strong"),
179 Self::Moderate => write!(f, "Moderate"),
180 Self::Weak => write!(f, "Weak"),
181 Self::Info => write!(f, "Info"),
182 }
183 }
184}
185
186impl std::str::FromStr for KnowledgeWeight {
187 type Err = ParseEnumError;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 match s {
191 "rule" => Ok(Self::Rule),
192 "strong" => Ok(Self::Strong),
193 "moderate" => Ok(Self::Moderate),
194 "weak" => Ok(Self::Weak),
195 "info" => Ok(Self::Info),
196 _ => Err(ParseEnumError {
197 type_name: "KnowledgeWeight",
198 value: s.to_owned(),
199 }),
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::ids::{BranchId, NodeId};
208
209 #[test]
210 fn knowledge_node_serialization_roundtrip() {
211 let node = KnowledgeNode {
212 id: NodeId(42),
213 branch_id: BranchId::from("main"),
214 nature: KnowledgeNature::Convention,
215 weight: KnowledgeWeight::Strong,
216 confidence: 0.92,
217 adoption_count: 23,
218 total_count: 25,
219 description: "Use thiserror for error types".to_owned(),
220 ext_data: None,
221 };
222
223 let json = serde_json::to_string(&node).expect("serialize");
224 assert!(!json.contains("ext_data"), "None fields should be skipped");
225
226 let deserialized: KnowledgeNode = serde_json::from_str(&json).expect("deserialize");
227 assert_eq!(deserialized.id, node.id);
228 assert_eq!(deserialized.nature, KnowledgeNature::Convention);
229 assert_eq!(deserialized.weight, KnowledgeWeight::Strong);
230 assert!((deserialized.confidence - 0.92).abs() < f64::EPSILON);
231 }
232
233 #[test]
234 fn knowledge_node_with_ext_data() {
235 let node = KnowledgeNode {
236 id: NodeId(1),
237 branch_id: BranchId::from("feature"),
238 nature: KnowledgeNature::Decision,
239 weight: KnowledgeWeight::Rule,
240 confidence: 1.0,
241 adoption_count: 1,
242 total_count: 1,
243 description: "Use SQLite for storage".to_owned(),
244 ext_data: Some(serde_json::json!({"reasoning": "Embedded, no runtime deps"})),
245 };
246
247 let json = serde_json::to_string(&node).expect("serialize");
248 assert!(json.contains("ext_data"));
249 assert!(json.contains("reasoning"));
250 }
251
252 #[test]
253 fn nature_and_weight_display() {
254 assert_eq!(KnowledgeNature::Convention.to_string(), "Convention");
255 assert_eq!(KnowledgeWeight::Strong.to_string(), "Strong");
256 }
257
258 #[test]
259 fn nature_roundtrip_str() {
260 let natures = [
261 KnowledgeNature::Fact,
262 KnowledgeNature::Convention,
263 KnowledgeNature::Observation,
264 KnowledgeNature::Decision,
265 KnowledgeNature::Preference,
266 ];
267 for n in natures {
268 let parsed: KnowledgeNature = n.as_str().parse().unwrap();
269 assert_eq!(parsed, n);
270 }
271 }
272
273 #[test]
274 fn weight_roundtrip_str() {
275 let weights = [
276 KnowledgeWeight::Rule,
277 KnowledgeWeight::Strong,
278 KnowledgeWeight::Moderate,
279 KnowledgeWeight::Weak,
280 KnowledgeWeight::Info,
281 ];
282 for w in weights {
283 let parsed: KnowledgeWeight = w.as_str().parse().unwrap();
284 assert_eq!(parsed, w);
285 }
286 }
287
288 #[test]
289 fn all_nature_variants() {
290 let natures = [
291 KnowledgeNature::Fact,
292 KnowledgeNature::Convention,
293 KnowledgeNature::Observation,
294 KnowledgeNature::Decision,
295 KnowledgeNature::Preference,
296 ];
297 assert_eq!(natures.len(), 5);
298 }
299
300 #[test]
301 fn all_weight_variants() {
302 let weights = [
303 KnowledgeWeight::Rule,
304 KnowledgeWeight::Strong,
305 KnowledgeWeight::Moderate,
306 KnowledgeWeight::Weak,
307 KnowledgeWeight::Info,
308 ];
309 assert_eq!(weights.len(), 5);
310 }
311
312 #[test]
313 fn trend_roundtrip_str() {
314 let trends = [
315 Trend::Rising,
316 Trend::Stable,
317 Trend::Declining,
318 Trend::Unknown,
319 ];
320 for t in trends {
321 let parsed: Trend = t.as_str().parse().unwrap();
322 assert_eq!(parsed, t);
323 }
324 }
325
326 #[test]
327 fn trend_display() {
328 assert_eq!(Trend::Rising.to_string(), "Rising");
329 assert_eq!(Trend::Stable.to_string(), "Stable");
330 assert_eq!(Trend::Declining.to_string(), "Declining");
331 assert_eq!(Trend::Unknown.to_string(), "Unknown");
332 }
333
334 #[test]
335 fn trend_serde_roundtrip() {
336 let trend = Trend::Rising;
337 let json = serde_json::to_string(&trend).expect("serialize");
338 assert_eq!(json, r#""rising""#);
339 let deserialized: Trend = serde_json::from_str(&json).expect("deserialize");
340 assert_eq!(deserialized, trend);
341 }
342
343 #[test]
344 fn all_trend_variants() {
345 let trends = [
346 Trend::Rising,
347 Trend::Stable,
348 Trend::Declining,
349 Trend::Unknown,
350 ];
351 assert_eq!(trends.len(), 4);
352 }
353}