lean_ctx/core/knowledge/
local_store.rs1use super::query::KnowledgeQuery;
4use super::store::{KnowledgeStore, KnowledgeStoreError, object_id, validate_object};
5use super::supersession::mark_superseded;
6use lean_ctx_protocol::KnowledgeObjectV1;
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, Default)]
11pub struct LocalKnowledgeStore {
12 objects: BTreeMap<String, KnowledgeObjectV1>,
13}
14
15impl LocalKnowledgeStore {
16 pub fn new() -> Self {
18 Self::default()
19 }
20
21 pub fn len(&self) -> usize {
23 self.objects.len()
24 }
25
26 pub fn is_empty(&self) -> bool {
28 self.objects.is_empty()
29 }
30
31 pub fn all(&self) -> Vec<KnowledgeObjectV1> {
33 self.objects.values().cloned().collect()
34 }
35
36 pub fn values(&self) -> impl Iterator<Item = &KnowledgeObjectV1> {
38 self.objects.values()
39 }
40
41 pub fn get_ref(&self, object_id: &str) -> Option<&KnowledgeObjectV1> {
43 self.objects.get(object_id)
44 }
45}
46
47impl KnowledgeStore for LocalKnowledgeStore {
48 fn get(&self, object_id: &str) -> Option<KnowledgeObjectV1> {
49 self.objects.get(object_id).cloned()
50 }
51
52 fn put(&mut self, object: KnowledgeObjectV1) -> Result<(), KnowledgeStoreError> {
53 validate_object(&object)?;
54 self.objects.insert(object_id(&object).to_owned(), object);
55 Ok(())
56 }
57
58 fn query(&self, query: &KnowledgeQuery) -> Vec<KnowledgeObjectV1> {
59 self.objects
60 .values()
61 .filter(|object| query.matches(object))
62 .cloned()
63 .collect()
64 }
65
66 fn supersede(
67 &mut self,
68 old_object_id: &str,
69 replacement: KnowledgeObjectV1,
70 ) -> Result<(), KnowledgeStoreError> {
71 validate_object(&replacement)?;
72 let replacement_id = object_id(&replacement).to_owned();
73 let old = self
74 .objects
75 .get_mut(old_object_id)
76 .ok_or_else(|| KnowledgeStoreError::NotFound(old_object_id.to_owned()))?;
77 mark_superseded(old, replacement_id);
78 self.objects
79 .insert(object_id(&replacement).to_owned(), replacement);
80 Ok(())
81 }
82
83 fn delete(&mut self, object_id: &str) -> bool {
84 self.objects.remove(object_id).is_some()
85 }
86}
87
88pub type InMemoryKnowledgeStore = LocalKnowledgeStore;
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::core::knowledge::query::KnowledgeQuery;
95 use chrono::{Duration, Utc};
96 use lean_ctx_protocol::ExtensionsV1;
97 use lean_ctx_protocol::knowledge::{
98 AuthorityMetadata, ClassificationLevel, DataClassification, KnowledgeSourceType,
99 SourceReference, ValidityWindow,
100 };
101
102 fn object(id: &str, tag: &str) -> KnowledgeObjectV1 {
103 KnowledgeObjectV1 {
104 schema_version: 1,
105 source_ref: Some(SourceReference {
106 source_type: "git".to_owned(),
107 uri: "repo://lean-ctx".to_owned(),
108 commit_sha: Some("abc123".to_owned()),
109 access_timestamp: "2026-08-09T00:00:00Z".to_owned(),
110 }),
111 source_type: KnowledgeSourceType::Repository,
112 authority: Some(AuthorityMetadata {
113 owner: "team".to_owned(),
114 confidence_level: 0.9,
115 review_status: "reviewed".to_owned(),
116 }),
117 owner: "team".to_owned(),
118 classification: Some(DataClassification {
119 level: ClassificationLevel::Internal,
120 retention_days: Some(30),
121 }),
122 validity: Some(ValidityWindow {
123 valid_from: (Utc::now() - Duration::hours(1)).to_rfc3339(),
124 valid_until: Some((Utc::now() + Duration::hours(1)).to_rfc3339()),
125 superseded_by: None,
126 }),
127 supersedes: None,
128 content_hash: id.to_owned(),
129 evidence_digest: format!("evidence:{id}"),
130 policy_ref: "policy:local".to_owned(),
131 evidence_refs: vec![format!("evidence-ref:{id}")],
132 extra: {
133 let mut extra = ExtensionsV1::default();
134 extra
135 .insert("tags", serde_json::json!([tag]))
136 .expect("extension should be valid");
137 extra
138 },
139 }
140 }
141
142 #[test]
143 fn crud_and_query_are_deterministic() {
144 let mut store = LocalKnowledgeStore::new();
145 store.put(object("b", "architecture")).expect("put b");
146 store.put(object("a", "architecture")).expect("put a");
147 assert_eq!(store.len(), 2);
148 assert_eq!(store.get("a").expect("a").object_id(), "a");
149
150 let query = KnowledgeQuery {
151 source: Some("repo://lean-ctx".to_owned()),
152 classification: Some(ClassificationLevel::Internal),
153 valid_at: Some(Utc::now()),
154 tags: vec!["architecture".to_owned()],
155 };
156 let results = store.query(&query);
157 assert_eq!(
158 results
159 .iter()
160 .map(KnowledgeObjectV1::object_id)
161 .collect::<Vec<_>>(),
162 ["a", "b"]
163 );
164 assert!(store.delete("a"));
165 assert!(!store.delete("a"));
166 }
167
168 #[test]
169 fn supersession_keeps_old_history_and_links_replacement() {
170 let mut store = LocalKnowledgeStore::new();
171 store.put(object("old", "architecture")).expect("put old");
172 store
173 .supersede("old", object("new", "architecture"))
174 .expect("supersede old");
175
176 let old = store.get("old").expect("old history remains");
177 assert_eq!(
178 old.validity
179 .as_ref()
180 .and_then(|validity| validity.superseded_by.as_deref()),
181 Some("new")
182 );
183 assert!(store.get("new").is_some());
184 }
185}