1use crate::ipld_codec::{
13 block_to_kb, fact_ipld_to_predicate, kb_to_block, predicate_to_fact_ipld, rule_cid,
14 rule_ipld_to_rule, rule_to_rule_ipld, KnowledgeBaseIpld,
15};
16use crate::ir::{KnowledgeBase, Predicate, Rule};
17use ipfrs_core::{Block, Cid, Result};
18use ipfrs_storage::traits::BlockStore;
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct KbMergeDiff {
26 pub facts_added: usize,
28 pub facts_skipped_duplicate: usize,
30 pub rules_added: usize,
32 pub rules_skipped_duplicate: usize,
34 pub conflicts: Vec<KbConflict>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub enum KbConflict {
41 RuleBodyConflict {
44 predicate: String,
46 local_rule: String,
48 remote_rule: String,
50 },
51}
52
53fn fact_content_hash(fact: &Predicate) -> u64 {
57 match predicate_to_fact_ipld(fact) {
58 Ok(ipld) => match serde_json::to_vec(&ipld) {
59 Ok(bytes) => fnv1a(&bytes),
60 Err(_) => {
61 fnv1a(format!("{:?}", fact).as_bytes())
63 }
64 },
65 Err(_) => fnv1a(format!("{:?}", fact).as_bytes()),
66 }
67}
68
69fn fnv1a(data: &[u8]) -> u64 {
70 const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
71 const FNV_PRIME: u64 = 1_099_511_628_211;
72 let mut hash = FNV_OFFSET;
73 for &byte in data {
74 hash ^= byte as u64;
75 hash = hash.wrapping_mul(FNV_PRIME);
76 }
77 hash
78}
79
80fn rule_content_hash(rule: &Rule) -> Option<u64> {
82 let ipld = rule_to_rule_ipld(rule).ok()?;
83 let bytes = serde_json::to_vec(&ipld).ok()?;
84 Some(fnv1a(&bytes))
85}
86
87pub fn merge_knowledge_bases(
97 local: &KnowledgeBase,
98 remote: &KnowledgeBase,
99) -> (KnowledgeBase, KbMergeDiff) {
100 let mut merged = local.clone();
101 let mut diff = KbMergeDiff::default();
102
103 let local_fact_hashes: std::collections::HashSet<u64> =
106 local.facts.iter().map(fact_content_hash).collect();
107
108 for remote_fact in &remote.facts {
109 let h = fact_content_hash(remote_fact);
110 if local_fact_hashes.contains(&h) {
111 diff.facts_skipped_duplicate += 1;
112 } else {
113 merged.add_fact(remote_fact.clone());
114 diff.facts_added += 1;
115 }
116 }
117
118 let local_rule_hashes: std::collections::HashMap<u64, &Rule> = local
121 .rules
122 .iter()
123 .filter_map(|r| rule_content_hash(r).map(|h| (h, r)))
124 .collect();
125
126 let local_signature_map: std::collections::HashMap<(String, usize), String> = local
128 .rules
129 .iter()
130 .map(|r| {
131 let key = (r.head.name.clone(), r.head.args.len());
132 let body_repr = rule_body_repr(r);
133 (key, body_repr)
134 })
135 .collect();
136
137 for remote_rule in &remote.rules {
138 let remote_hash = match rule_content_hash(remote_rule) {
139 Some(h) => h,
140 None => {
141 merged.add_rule(remote_rule.clone());
143 diff.rules_added += 1;
144 continue;
145 }
146 };
147
148 if local_rule_hashes.contains_key(&remote_hash) {
149 diff.rules_skipped_duplicate += 1;
150 continue;
151 }
152
153 let sig = (remote_rule.head.name.clone(), remote_rule.head.args.len());
155 if let Some(local_body_repr) = local_signature_map.get(&sig) {
156 let remote_body_repr = rule_body_repr(remote_rule);
157 if *local_body_repr != remote_body_repr {
158 diff.conflicts.push(KbConflict::RuleBodyConflict {
160 predicate: remote_rule.head.name.clone(),
161 local_rule: format!(
162 "{}/{}: {}",
163 remote_rule.head.name,
164 remote_rule.head.args.len(),
165 local_body_repr
166 ),
167 remote_rule: format!(
168 "{}/{}: {}",
169 remote_rule.head.name,
170 remote_rule.head.args.len(),
171 remote_body_repr
172 ),
173 });
174
175 let mut renamed = remote_rule.clone();
178 renamed.head.name = format!("remote_{}", remote_rule.head.name);
179 merged.add_rule(renamed);
180 diff.rules_added += 1;
181 continue;
182 }
183 }
184
185 merged.add_rule(remote_rule.clone());
186 diff.rules_added += 1;
187 }
188
189 (merged, diff)
190}
191
192fn rule_body_repr(rule: &Rule) -> String {
194 rule.body
195 .iter()
196 .map(|p| format!("{}({})", p.name, p.args.len()))
197 .collect::<Vec<_>>()
198 .join(",")
199}
200
201pub async fn export_kb_as_cid<S: BlockStore>(
210 store: &crate::storage::TensorLogicStore<S>,
211 block_store: &dyn BlockStore,
212) -> Result<Cid> {
213 let kb = store.snapshot_kb()?;
215
216 let mut rule_cids: Vec<String> = Vec::with_capacity(kb.rules.len());
218 for rule in &kb.rules {
219 let rule_ipld = rule_to_rule_ipld(rule)?;
220 let cid = rule_cid(&rule_ipld)?;
221
222 let json_bytes = serde_json::to_vec(&rule_ipld)
224 .map_err(|e| ipfrs_core::Error::Serialization(format!("rule IPLD: {}", e)))?;
225 let block = Block::from_parts(cid, bytes::Bytes::from(json_bytes));
226 block_store.put(&block).await?;
227
228 rule_cids.push(cid.to_string());
229 }
230
231 let fact_iplds = kb
233 .facts
234 .iter()
235 .map(predicate_to_fact_ipld)
236 .collect::<std::result::Result<Vec<_>, _>>()?;
237
238 let kb_ipld = KnowledgeBaseIpld {
239 rules: rule_cids,
240 facts: fact_iplds,
241 version: "1.0.0".to_string(),
242 };
243
244 let root_block = kb_to_block(&kb_ipld)?;
245 let root_cid = *root_block.cid();
246 block_store.put(&root_block).await?;
247
248 Ok(root_cid)
249}
250
251pub async fn import_remote_kb<S: BlockStore>(
262 remote_cid: &Cid,
263 store: &crate::storage::TensorLogicStore<S>,
264 block_store: &dyn BlockStore,
265) -> Result<KbMergeDiff> {
266 let root_block = block_store
268 .get(remote_cid)
269 .await?
270 .ok_or_else(|| ipfrs_core::Error::BlockNotFound(remote_cid.to_string()))?;
271
272 let kb_ipld = block_to_kb(&root_block)?;
273
274 let mut remote_kb = KnowledgeBase::new();
276
277 for rule_cid_str in &kb_ipld.rules {
279 let rule_cid: Cid = rule_cid_str
280 .parse()
281 .map_err(|e| ipfrs_core::Error::Cid(format!("invalid CID {}: {}", rule_cid_str, e)))?;
282
283 let rule_block = block_store
284 .get(&rule_cid)
285 .await?
286 .ok_or_else(|| ipfrs_core::Error::BlockNotFound(rule_cid.to_string()))?;
287
288 let rule_ipld = crate::ipld_codec::block_to_rule(&rule_block)?;
289 let rule = rule_ipld_to_rule(&rule_ipld)?;
290 remote_kb.add_rule(rule);
291 }
292
293 for fact_ipld in &kb_ipld.facts {
295 let predicate = fact_ipld_to_predicate(fact_ipld)?;
296 remote_kb.add_fact(predicate);
297 }
298
299 let local_kb = store.snapshot_kb()?;
301
302 let (merged_kb, diff) = merge_knowledge_bases(&local_kb, &remote_kb);
304
305 for fact in &merged_kb.facts {
307 let local_fact_hashes: std::collections::HashSet<u64> =
309 local_kb.facts.iter().map(fact_content_hash).collect();
310 if !local_fact_hashes.contains(&fact_content_hash(fact)) {
311 store.add_fact(fact.clone())?;
312 }
313 }
314
315 for rule in &merged_kb.rules {
316 let local_rule_hashes: std::collections::HashSet<u64> = local_kb
317 .rules
318 .iter()
319 .filter_map(rule_content_hash)
320 .collect();
321 if let Some(h) = rule_content_hash(rule) {
322 if !local_rule_hashes.contains(&h) {
323 store.add_rule(rule.clone())?;
324 }
325 }
326 }
327
328 Ok(diff)
329}
330
331#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::ir::{Constant, Predicate, Rule, Term};
337 use crate::storage::TensorLogicStore;
338 use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
339 use std::sync::Arc;
340
341 fn make_store(suffix: &str) -> Arc<TensorLogicStore<SledBlockStore>> {
342 let path = std::env::temp_dir().join(format!("ipfrs-test-kb-fed-{}", suffix));
343 let _ = std::fs::remove_dir_all(&path);
344 let config = BlockStoreConfig {
345 path,
346 cache_size: 32 * 1024 * 1024,
347 };
348 let sled = Arc::new(SledBlockStore::new(config).expect("sled store"));
349 Arc::new(TensorLogicStore::new(sled).expect("tensorlogic store"))
350 }
351
352 fn make_sled_store(suffix: &str) -> Arc<SledBlockStore> {
353 let path = std::env::temp_dir().join(format!("ipfrs-test-sled-fed-{}", suffix));
354 let _ = std::fs::remove_dir_all(&path);
355 let config = BlockStoreConfig {
356 path,
357 cache_size: 16 * 1024 * 1024,
358 };
359 Arc::new(SledBlockStore::new(config).expect("sled store"))
360 }
361
362 fn parent_fact(a: &str, b: &str) -> Predicate {
363 Predicate::new(
364 "parent".to_string(),
365 vec![
366 Term::Const(Constant::String(a.to_string())),
367 Term::Const(Constant::String(b.to_string())),
368 ],
369 )
370 }
371
372 fn ancestor_rule_base() -> Rule {
373 Rule::new(
374 Predicate::new(
375 "ancestor".to_string(),
376 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
377 ),
378 vec![Predicate::new(
379 "parent".to_string(),
380 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
381 )],
382 )
383 }
384
385 fn sibling_rule() -> Rule {
386 Rule::new(
387 Predicate::new(
388 "sibling".to_string(),
389 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
390 ),
391 vec![
392 Predicate::new(
393 "parent".to_string(),
394 vec![Term::Var("Z".to_string()), Term::Var("X".to_string())],
395 ),
396 Predicate::new(
397 "parent".to_string(),
398 vec![Term::Var("Z".to_string()), Term::Var("Y".to_string())],
399 ),
400 ],
401 )
402 }
403
404 #[test]
405 fn test_merge_disjoint_kbs() {
406 let mut local_kb = KnowledgeBase::new();
407 local_kb.add_fact(parent_fact("alice", "bob"));
408 local_kb.add_rule(ancestor_rule_base());
409
410 let mut remote_kb = KnowledgeBase::new();
411 remote_kb.add_fact(parent_fact("bob", "charlie"));
412 remote_kb.add_rule(sibling_rule());
413
414 let (merged, diff) = merge_knowledge_bases(&local_kb, &remote_kb);
415
416 assert_eq!(merged.facts.len(), 2, "should have both facts");
417 assert_eq!(merged.rules.len(), 2, "should have both rules");
418 assert_eq!(diff.facts_added, 1);
419 assert_eq!(diff.facts_skipped_duplicate, 0);
420 assert_eq!(diff.rules_added, 1);
421 assert_eq!(diff.rules_skipped_duplicate, 0);
422 assert!(diff.conflicts.is_empty());
423 }
424
425 #[test]
426 fn test_merge_duplicate_facts() {
427 let mut local_kb = KnowledgeBase::new();
428 local_kb.add_fact(parent_fact("alice", "bob"));
429
430 let mut remote_kb = KnowledgeBase::new();
431 remote_kb.add_fact(parent_fact("alice", "bob"));
433 remote_kb.add_fact(parent_fact("bob", "charlie"));
435
436 let (merged, diff) = merge_knowledge_bases(&local_kb, &remote_kb);
437
438 assert_eq!(merged.facts.len(), 2);
439 assert_eq!(
440 diff.facts_skipped_duplicate, 1,
441 "duplicate should be skipped"
442 );
443 assert_eq!(diff.facts_added, 1);
444 }
445
446 #[test]
447 fn test_merge_conflicting_rules() {
448 let mut local_kb = KnowledgeBase::new();
449 local_kb.add_rule(ancestor_rule_base());
451
452 let mut remote_kb = KnowledgeBase::new();
453 let conflict_rule = Rule::new(
455 Predicate::new(
456 "ancestor".to_string(),
457 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
458 ),
459 vec![
460 Predicate::new(
461 "ancestor".to_string(),
462 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
463 ),
464 Predicate::new(
465 "ancestor".to_string(),
466 vec![Term::Var("Z".to_string()), Term::Var("Y".to_string())],
467 ),
468 ],
469 );
470 remote_kb.add_rule(conflict_rule);
471
472 let (_merged, diff) = merge_knowledge_bases(&local_kb, &remote_kb);
473
474 assert_eq!(diff.conflicts.len(), 1, "should have one conflict");
475 match &diff.conflicts[0] {
476 KbConflict::RuleBodyConflict { predicate, .. } => {
477 assert_eq!(predicate, "ancestor");
478 }
479 }
480 assert_eq!(diff.rules_added, 1);
482 }
483
484 #[tokio::test]
485 async fn test_export_import_roundtrip() {
486 let local_store = make_store("roundtrip-local");
487 let block_store = make_sled_store("roundtrip-blocks");
488
489 local_store
491 .add_fact(parent_fact("alice", "bob"))
492 .expect("add fact");
493 local_store
494 .add_rule(ancestor_rule_base())
495 .expect("add rule");
496
497 let cid = export_kb_as_cid(&local_store, block_store.as_ref())
499 .await
500 .expect("export");
501
502 let fresh_store = make_store("roundtrip-fresh");
504 let diff = import_remote_kb(&cid, &fresh_store, block_store.as_ref())
505 .await
506 .expect("import");
507
508 assert_eq!(diff.facts_added, 1, "should have imported 1 fact");
509 assert_eq!(diff.rules_added, 1, "should have imported 1 rule");
510
511 let kb = fresh_store.snapshot_kb().expect("snapshot");
512 assert_eq!(kb.facts.len(), 1);
513 assert_eq!(kb.rules.len(), 1);
514 assert_eq!(kb.facts[0].name, "parent");
515 assert_eq!(kb.rules[0].head.name, "ancestor");
516 }
517}