1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3
4use super::types::{KnowledgeArchetype, KnowledgeFact, ProjectKnowledge};
5use crate::core::memory_boundary::FactPrivacy;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ImportMerge {
9 Replace,
10 Append,
11 SkipExisting,
12}
13
14impl ImportMerge {
15 pub fn parse(s: &str) -> Option<Self> {
16 match s.to_lowercase().as_str() {
17 "replace" => Some(Self::Replace),
18 "append" => Some(Self::Append),
19 "skip-existing" | "skip_existing" | "skip" => Some(Self::SkipExisting),
20 _ => None,
21 }
22 }
23}
24
25#[derive(Debug, Clone)]
26pub struct ImportResult {
27 pub added: u32,
28 pub skipped: u32,
29 pub replaced: u32,
30 pub conflicts: Vec<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SimpleFactEntry {
39 pub category: String,
40 pub key: String,
41 pub value: String,
42 #[serde(default)]
43 pub confidence: Option<f32>,
44 #[serde(default)]
45 pub source: Option<String>,
46 #[serde(default)]
47 pub timestamp: Option<String>,
48}
49
50pub fn parse_import_data(data: &str) -> Result<Vec<KnowledgeFact>, String> {
52 if let Ok(mut pk) = serde_json::from_str::<ProjectKnowledge>(data) {
53 for fact in &mut pk.facts {
54 fact.origin = super::source_view::imported_origin(&fact.origin);
55 }
56 return Ok(pk.facts);
57 }
58
59 if let Ok(entries) = serde_json::from_str::<Vec<SimpleFactEntry>>(data) {
60 let now = Utc::now();
61 let facts = entries
62 .into_iter()
63 .map(|e| KnowledgeFact {
64 anchors: Vec::new(),
65 review: None,
66 capture: Default::default(),
67 origin: super::FactOrigin::Unverified,
68 archetype: KnowledgeArchetype::infer_from_category(&e.category),
69 sensitivity: crate::core::sensitivity::classify_content(&e.value),
70 category: e.category,
71 key: e.key,
72 value: e.value,
73 source_session: e.source.unwrap_or_else(|| "import".to_string()),
74 confidence: e.confidence.unwrap_or(0.8),
75 created_at: now,
76 last_confirmed: now,
77 retrieval_count: 0,
78 last_retrieved: None,
79 valid_from: Some(now),
80 valid_until: None,
81 supersedes: None,
82 confirmation_count: 1,
83 feedback_up: 0,
84 feedback_down: 0,
85 last_feedback: None,
86 privacy: FactPrivacy::default(),
87 imported_from: None,
88 fidelity: None,
89 revision_count: 0,
90 status: None,
91 validity: None,
92 })
93 .collect();
94 return Ok(facts);
95 }
96
97 let mut facts = Vec::new();
98 for line in data.lines() {
99 let line = line.trim();
100 if line.is_empty() {
101 continue;
102 }
103 if let Ok(entry) = serde_json::from_str::<SimpleFactEntry>(line) {
104 let now = Utc::now();
105 facts.push(KnowledgeFact {
106 anchors: Vec::new(),
107 review: None,
108 capture: Default::default(),
109 origin: super::FactOrigin::Unverified,
110 archetype: KnowledgeArchetype::infer_from_category(&entry.category),
111 sensitivity: crate::core::sensitivity::classify_content(&entry.value),
112 category: entry.category,
113 key: entry.key,
114 value: entry.value,
115 source_session: entry.source.unwrap_or_else(|| "import".to_string()),
116 confidence: entry.confidence.unwrap_or(0.8),
117 created_at: now,
118 last_confirmed: now,
119 retrieval_count: 0,
120 last_retrieved: None,
121 valid_from: Some(now),
122 valid_until: None,
123 supersedes: None,
124 confirmation_count: 1,
125 feedback_up: 0,
126 feedback_down: 0,
127 last_feedback: None,
128 privacy: FactPrivacy::default(),
129 imported_from: None,
130 fidelity: None,
131 revision_count: 0,
132 status: None,
133 validity: None,
134 });
135 } else {
136 return Err(format!(
137 "Invalid JSONL line: {}",
138 &line[..line.len().min(80)]
139 ));
140 }
141 }
142
143 if facts.is_empty() {
144 return Err("No facts found. Expected: native JSON, simple JSON array, or JSONL.".into());
145 }
146 Ok(facts)
147}
148
149fn imported_fact(source: &KnowledgeFact, session_id: &str) -> KnowledgeFact {
150 let now = Utc::now();
151 KnowledgeFact {
152 anchors: Vec::new(),
153 review: None,
154 capture: Default::default(),
155 origin: super::source_view::imported_origin(&source.origin),
156 category: source.category.clone(),
157 key: source.key.clone(),
158 value: source.value.clone(),
159 source_session: session_id.to_string(),
160 confidence: source.confidence,
161 created_at: now,
162 last_confirmed: now,
163 retrieval_count: 0,
164 last_retrieved: None,
165 valid_from: Some(now),
166 valid_until: None,
167 supersedes: None,
168 confirmation_count: 1,
169 feedback_up: 0,
170 feedback_down: 0,
171 last_feedback: None,
172 privacy: source.privacy,
173 sensitivity: source.sensitivity,
174 imported_from: source.imported_from.clone(),
175 archetype: source.archetype.clone(),
176 fidelity: None,
177 revision_count: 0,
178 status: None,
179 validity: source.validity,
180 }
181}
182
183impl ProjectKnowledge {
184 pub fn import_facts(
187 &mut self,
188 incoming: Vec<KnowledgeFact>,
189 merge: ImportMerge,
190 session_id: &str,
191 ) -> ImportResult {
192 let mut added = 0u32;
193 let mut skipped = 0u32;
194 let mut replaced = 0u32;
195 let mut conflicts = Vec::new();
196
197 for fact in incoming {
198 let existing = self
199 .facts
200 .iter()
201 .position(|f| f.category == fact.category && f.key == fact.key && f.is_current());
202
203 match (&merge, existing) {
204 (ImportMerge::SkipExisting, Some(idx)) => {
205 if self.facts[idx].value != fact.value {
206 conflicts.push(format!("{}/{}", fact.category, fact.key));
207 }
208 skipped += 1;
209 }
210 (ImportMerge::Replace, Some(idx)) => {
211 self.facts[idx].valid_until = Some(Utc::now());
212 self.facts.push(imported_fact(&fact, session_id));
213 replaced += 1;
214 }
215 (ImportMerge::Append, Some(_)) | (_, None) => {
216 self.facts.push(imported_fact(&fact, session_id));
217 added += 1;
218 }
219 }
220 }
221
222 if added > 0 || replaced > 0 {
223 self.updated_at = Utc::now();
224 self.rebuild_index();
225 }
226
227 ImportResult {
228 added,
229 skipped,
230 replaced,
231 conflicts,
232 }
233 }
234
235 pub fn export_simple(&self) -> Vec<SimpleFactEntry> {
237 self.facts
238 .iter()
239 .filter(|f| f.is_current())
240 .map(|f| SimpleFactEntry {
241 category: f.category.clone(),
242 key: f.key.clone(),
243 value: f.value.clone(),
244 confidence: Some(f.confidence),
245 source: Some(f.source_session.clone()),
246 timestamp: Some(f.created_at.to_rfc3339()),
247 })
248 .collect()
249 }
250}