chronicle/schema/
knowledge.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use super::v2::Stability;
5
6#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
8pub struct KnowledgeStore {
9 pub schema: String, #[serde(default)]
11 pub conventions: Vec<Convention>,
12 #[serde(default)]
13 pub boundaries: Vec<ModuleBoundary>,
14 #[serde(default)]
15 pub anti_patterns: Vec<AntiPattern>,
16}
17
18impl KnowledgeStore {
19 pub fn new() -> Self {
20 Self {
21 schema: "chronicle/knowledge-v1".to_string(),
22 conventions: Vec::new(),
23 boundaries: Vec::new(),
24 anti_patterns: Vec::new(),
25 }
26 }
27
28 pub fn is_empty(&self) -> bool {
29 self.conventions.is_empty() && self.boundaries.is_empty() && self.anti_patterns.is_empty()
30 }
31
32 pub fn remove_by_id(&mut self, id: &str) -> bool {
34 let len_before = self.conventions.len() + self.boundaries.len() + self.anti_patterns.len();
35
36 self.conventions.retain(|c| c.id != id);
37 self.boundaries.retain(|b| b.id != id);
38 self.anti_patterns.retain(|a| a.id != id);
39
40 let len_after = self.conventions.len() + self.boundaries.len() + self.anti_patterns.len();
41 len_after < len_before
42 }
43}
44
45impl Default for KnowledgeStore {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53pub struct Convention {
54 pub id: String,
55 pub scope: String,
56 pub rule: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub decided_in: Option<String>,
59 pub stability: Stability,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
64pub struct ModuleBoundary {
65 pub id: String,
66 pub module: String,
67 pub owns: String,
68 pub boundary: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub decided_in: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
75pub struct AntiPattern {
76 pub id: String,
77 pub pattern: String,
78 pub instead: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub learned_from: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize)]
85pub struct FilteredKnowledge {
86 pub conventions: Vec<Convention>,
87 pub boundaries: Vec<ModuleBoundary>,
88 pub anti_patterns: Vec<AntiPattern>,
89}
90
91impl FilteredKnowledge {
92 pub fn is_empty(&self) -> bool {
93 self.conventions.is_empty() && self.boundaries.is_empty() && self.anti_patterns.is_empty()
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_knowledge_store_new() {
103 let store = KnowledgeStore::new();
104 assert_eq!(store.schema, "chronicle/knowledge-v1");
105 assert!(store.is_empty());
106 }
107
108 #[test]
109 fn test_knowledge_store_remove_by_id() {
110 let mut store = KnowledgeStore::new();
111 store.conventions.push(Convention {
112 id: "conv-1".to_string(),
113 scope: "src/".to_string(),
114 rule: "Use snafu for errors".to_string(),
115 decided_in: None,
116 stability: Stability::Permanent,
117 });
118 store.anti_patterns.push(AntiPattern {
119 id: "ap-1".to_string(),
120 pattern: "unwrap() in production code".to_string(),
121 instead: "Use proper error handling".to_string(),
122 learned_from: None,
123 });
124
125 assert!(store.remove_by_id("conv-1"));
126 assert!(store.conventions.is_empty());
127 assert_eq!(store.anti_patterns.len(), 1);
128
129 assert!(!store.remove_by_id("nonexistent"));
130 }
131
132 #[test]
133 fn test_knowledge_store_roundtrip() {
134 let mut store = KnowledgeStore::new();
135 store.conventions.push(Convention {
136 id: "conv-1".to_string(),
137 scope: "src/schema/".to_string(),
138 rule: "Use parse_annotation() for all deserialization".to_string(),
139 decided_in: Some("abc123".to_string()),
140 stability: Stability::Permanent,
141 });
142 store.boundaries.push(ModuleBoundary {
143 id: "bound-1".to_string(),
144 module: "src/git/".to_string(),
145 owns: "All git operations".to_string(),
146 boundary: "Must not import from provider module".to_string(),
147 decided_in: None,
148 });
149 store.anti_patterns.push(AntiPattern {
150 id: "ap-1".to_string(),
151 pattern: "serde_json::from_str for annotations".to_string(),
152 instead: "Use schema::parse_annotation()".to_string(),
153 learned_from: Some("BUG-42".to_string()),
154 });
155
156 let json = serde_json::to_string_pretty(&store).unwrap();
157 let parsed: KnowledgeStore = serde_json::from_str(&json).unwrap();
158 assert_eq!(parsed.conventions.len(), 1);
159 assert_eq!(parsed.boundaries.len(), 1);
160 assert_eq!(parsed.anti_patterns.len(), 1);
161 assert_eq!(parsed.conventions[0].id, "conv-1");
162 }
163}