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