1use super::fact::{
7 PendingFactSubmission, PersonalFact, PersonalFactCategory, PersonalFactFeedback,
8};
9use anyhow::Result;
10use rusqlite::{Connection, OptionalExtension, params};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::{Arc, Mutex};
14
15pub struct PersonalKnowledgeCache {
17 conn: Arc<Mutex<Connection>>,
19
20 facts: HashMap<String, PersonalFact>,
22
23 facts_by_key: HashMap<String, String>, pub last_sync: i64,
28
29 pending_submissions: Vec<PendingFactSubmission>,
31
32 pending_feedback: Vec<PersonalFactFeedback>,
34
35 max_queue_size: usize,
37}
38
39impl PersonalKnowledgeCache {
40 pub fn new<P: AsRef<Path>>(db_path: P, max_queue_size: usize) -> Result<Self> {
42 let conn = Connection::open(db_path)?;
43 Self::init_schema(&conn)?;
44
45 let mut cache = Self {
46 conn: Arc::new(Mutex::new(conn)),
47 facts: HashMap::new(),
48 facts_by_key: HashMap::new(),
49 last_sync: 0,
50 pending_submissions: Vec::new(),
51 pending_feedback: Vec::new(),
52 max_queue_size,
53 };
54
55 cache.load_from_db()?;
57
58 Ok(cache)
59 }
60
61 pub fn in_memory(max_queue_size: usize) -> Result<Self> {
63 let conn = Connection::open_in_memory()?;
64 Self::init_schema(&conn)?;
65
66 Ok(Self {
67 conn: Arc::new(Mutex::new(conn)),
68 facts: HashMap::new(),
69 facts_by_key: HashMap::new(),
70 last_sync: 0,
71 pending_submissions: Vec::new(),
72 pending_feedback: Vec::new(),
73 max_queue_size,
74 })
75 }
76
77 fn init_schema(conn: &Connection) -> Result<()> {
79 conn.execute_batch(
80 r#"
81 CREATE TABLE IF NOT EXISTS personal_facts (
82 id TEXT PRIMARY KEY,
83 category TEXT NOT NULL,
84 key TEXT NOT NULL UNIQUE,
85 value TEXT NOT NULL,
86 context TEXT,
87 confidence REAL NOT NULL,
88 reinforcements INTEGER NOT NULL DEFAULT 0,
89 contradictions INTEGER NOT NULL DEFAULT 0,
90 last_used INTEGER NOT NULL,
91 created_at INTEGER NOT NULL,
92 updated_at INTEGER NOT NULL,
93 source TEXT NOT NULL,
94 version INTEGER NOT NULL DEFAULT 1,
95 deleted INTEGER NOT NULL DEFAULT 0,
96 local_only INTEGER NOT NULL DEFAULT 0
97 );
98
99 CREATE INDEX IF NOT EXISTS idx_personal_facts_key ON personal_facts(key);
100 CREATE INDEX IF NOT EXISTS idx_personal_facts_category ON personal_facts(category);
101 CREATE INDEX IF NOT EXISTS idx_personal_facts_confidence ON personal_facts(confidence);
102
103 CREATE TABLE IF NOT EXISTS pending_fact_submissions (
104 id INTEGER PRIMARY KEY AUTOINCREMENT,
105 fact_json TEXT NOT NULL,
106 queued_at INTEGER NOT NULL,
107 attempts INTEGER NOT NULL DEFAULT 0,
108 last_error TEXT
109 );
110
111 CREATE TABLE IF NOT EXISTS pending_fact_feedback (
112 id INTEGER PRIMARY KEY AUTOINCREMENT,
113 fact_id TEXT NOT NULL,
114 is_reinforcement INTEGER NOT NULL,
115 context TEXT,
116 timestamp INTEGER NOT NULL
117 );
118
119 CREATE TABLE IF NOT EXISTS personal_sync_state (
120 key TEXT PRIMARY KEY,
121 value TEXT NOT NULL
122 );
123 "#,
124 )?;
125
126 Ok(())
127 }
128
129 fn load_from_db(&mut self) -> Result<()> {
131 let conn = self
132 .conn
133 .lock()
134 .expect("personal knowledge cache connection lock poisoned");
135
136 self.last_sync = conn
138 .query_row(
139 "SELECT value FROM personal_sync_state WHERE key = 'last_sync'",
140 [],
141 |row| row.get::<_, String>(0),
142 )
143 .optional()?
144 .and_then(|s| s.parse().ok())
145 .unwrap_or(0);
146
147 let mut stmt = conn.prepare(
149 "SELECT id, category, key, value, context, confidence,
150 reinforcements, contradictions, last_used, created_at,
151 updated_at, source, version, deleted, local_only
152 FROM personal_facts WHERE deleted = 0",
153 )?;
154
155 let facts = stmt.query_map([], |row| {
156 Ok(PersonalFact {
157 id: row.get(0)?,
158 category: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(1)?))
159 .unwrap_or(PersonalFactCategory::Preference),
160 key: row.get(2)?,
161 value: row.get(3)?,
162 context: row.get(4)?,
163 confidence: row.get(5)?,
164 reinforcements: row.get(6)?,
165 contradictions: row.get(7)?,
166 last_used: row.get(8)?,
167 created_at: row.get(9)?,
168 updated_at: row.get(10)?,
169 source: serde_json::from_str(&format!("\"{}\"", row.get::<_, String>(11)?))
170 .unwrap_or(super::fact::PersonalFactSource::ExplicitStatement),
171 version: row.get::<_, i64>(12)? as u64,
172 deleted: row.get::<_, i32>(13)? != 0,
173 local_only: row.get::<_, i32>(14)? != 0,
174 })
175 })?;
176
177 for fact in facts {
178 let fact = fact?;
179 self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
180 self.facts.insert(fact.id.clone(), fact);
181 }
182
183 let mut stmt = conn.prepare(
185 "SELECT fact_json, queued_at, attempts, last_error FROM pending_fact_submissions",
186 )?;
187
188 let submissions = stmt.query_map([], |row| {
189 let json: String = row.get(0)?;
190 let fact: PersonalFact = serde_json::from_str(&json).map_err(|e| {
191 rusqlite::Error::FromSqlConversionFailure(
192 0,
193 rusqlite::types::Type::Text,
194 Box::new(e),
195 )
196 })?;
197 Ok(PendingFactSubmission {
198 fact,
199 queued_at: row.get(1)?,
200 attempts: row.get(2)?,
201 last_error: row.get(3)?,
202 })
203 })?;
204
205 for submission in submissions {
206 self.pending_submissions.push(submission?);
207 }
208
209 let mut stmt = conn.prepare(
211 "SELECT fact_id, is_reinforcement, context, timestamp FROM pending_fact_feedback",
212 )?;
213
214 let feedback = stmt.query_map([], |row| {
215 Ok(PersonalFactFeedback {
216 fact_id: row.get(0)?,
217 is_reinforcement: row.get::<_, i32>(1)? != 0,
218 context: row.get(2)?,
219 timestamp: row.get(3)?,
220 })
221 })?;
222
223 for fb in feedback {
224 self.pending_feedback.push(fb?);
225 }
226
227 Ok(())
228 }
229
230 fn save_fact_to_db(&self, fact: &PersonalFact) -> Result<()> {
232 let conn = self
233 .conn
234 .lock()
235 .expect("personal knowledge cache connection lock poisoned");
236 let category = serde_json::to_string(&fact.category)?
237 .trim_matches('"')
238 .to_string();
239 let source = serde_json::to_string(&fact.source)?
240 .trim_matches('"')
241 .to_string();
242
243 conn.execute(
244 r#"INSERT OR REPLACE INTO personal_facts
245 (id, category, key, value, context, confidence,
246 reinforcements, contradictions, last_used, created_at,
247 updated_at, source, version, deleted, local_only)
248 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)"#,
249 params![
250 fact.id,
251 category,
252 fact.key,
253 fact.value,
254 fact.context,
255 fact.confidence,
256 fact.reinforcements,
257 fact.contradictions,
258 fact.last_used,
259 fact.created_at,
260 fact.updated_at,
261 source,
262 fact.version as i64,
263 fact.deleted as i32,
264 fact.local_only as i32,
265 ],
266 )?;
267
268 Ok(())
269 }
270
271 pub fn set_last_sync(&mut self, timestamp: i64) -> Result<()> {
273 self.last_sync = timestamp;
274 let conn = self
275 .conn
276 .lock()
277 .expect("personal knowledge cache connection lock poisoned");
278 conn.execute(
279 "INSERT OR REPLACE INTO personal_sync_state (key, value) VALUES ('last_sync', ?1)",
280 params![timestamp.to_string()],
281 )?;
282 Ok(())
283 }
284
285 pub fn upsert_fact(&mut self, mut fact: PersonalFact) -> Result<()> {
287 if let Some(existing_id) = self.facts_by_key.get(&fact.key)
289 && let Some(existing) = self.facts.get(existing_id)
290 {
291 fact.id = existing.id.clone();
293 fact.reinforcements = existing.reinforcements + 1;
294 fact.confidence = fact.confidence.max(existing.confidence);
295 }
296
297 self.save_fact_to_db(&fact)?;
298 self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
299 self.facts.insert(fact.id.clone(), fact);
300 Ok(())
301 }
302
303 pub fn add_fact(&mut self, fact: PersonalFact) -> Result<()> {
305 self.save_fact_to_db(&fact)?;
306 self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
307 self.facts.insert(fact.id.clone(), fact);
308 Ok(())
309 }
310
311 pub fn upsert_fact_simple(
313 &mut self,
314 key: &str,
315 value: &str,
316 _confidence: f32,
317 local_only: bool,
318 ) -> Result<()> {
319 use super::fact::{PersonalFactCategory, PersonalFactSource};
320
321 let fact = PersonalFact::new(
322 PersonalFactCategory::Context,
323 key.to_string(),
324 value.to_string(),
325 None,
326 PersonalFactSource::SystemObserved,
327 local_only,
328 );
329
330 self.upsert_fact(fact)
331 }
332
333 pub fn get_all_facts(&self) -> Vec<&PersonalFact> {
335 self.facts.values().filter(|f| !f.deleted).collect()
336 }
337
338 pub fn get_facts_by_key_prefix(&self, prefix: &str) -> Result<Vec<&PersonalFact>> {
340 Ok(self
341 .facts
342 .values()
343 .filter(|f| !f.deleted && f.key.starts_with(prefix))
344 .collect())
345 }
346
347 pub fn update_fact(&mut self, fact: PersonalFact) -> Result<()> {
349 self.save_fact_to_db(&fact)?;
350 self.facts_by_key.insert(fact.key.clone(), fact.id.clone());
351 self.facts.insert(fact.id.clone(), fact);
352 Ok(())
353 }
354
355 pub fn get_fact(&self, id: &str) -> Option<&PersonalFact> {
357 self.facts.get(id)
358 }
359
360 pub fn get_fact_by_key(&self, key: &str) -> Option<&PersonalFact> {
362 self.facts_by_key.get(key).and_then(|id| self.facts.get(id))
363 }
364
365 pub fn get_fact_mut(&mut self, id: &str) -> Option<&mut PersonalFact> {
367 self.facts.get_mut(id)
368 }
369
370 pub fn remove_fact(&mut self, id: &str) -> Result<bool> {
372 let key_to_remove = {
374 if let Some(fact) = self.facts.get_mut(id) {
375 fact.delete();
376 Some(fact.key.clone())
377 } else {
378 None
379 }
380 };
381
382 if let Some(key) = key_to_remove {
384 if let Some(fact) = self.facts.get(id) {
385 self.save_fact_to_db(fact)?;
386 }
387 self.facts_by_key.remove(&key);
388 return Ok(true);
389 }
390 Ok(false)
391 }
392
393 pub fn remove_fact_by_key(&mut self, key: &str) -> Result<bool> {
395 if let Some(id) = self.facts_by_key.get(key).cloned() {
396 return self.remove_fact(&id);
397 }
398 Ok(false)
399 }
400
401 pub fn all_facts(&self) -> impl Iterator<Item = &PersonalFact> {
403 self.facts.values().filter(|f| !f.deleted)
404 }
405
406 pub fn facts_by_category(&self, category: PersonalFactCategory) -> Vec<&PersonalFact> {
408 self.facts
409 .values()
410 .filter(|f| !f.deleted && f.category == category)
411 .collect()
412 }
413
414 pub fn search_facts(&self, query: &str) -> Vec<&PersonalFact> {
416 let query_lower = query.to_lowercase();
417 self.facts
418 .values()
419 .filter(|f| {
420 !f.deleted
421 && (f.key.to_lowercase().contains(&query_lower)
422 || f.value.to_lowercase().contains(&query_lower))
423 })
424 .collect()
425 }
426
427 pub fn get_reliable_facts(&self, min_confidence: f32) -> Vec<&PersonalFact> {
429 self.facts
430 .values()
431 .filter(|f| !f.deleted && f.is_reliable(min_confidence))
432 .collect()
433 }
434
435 pub fn get_syncable_facts(&self) -> Vec<&PersonalFact> {
437 self.facts
438 .values()
439 .filter(|f| !f.deleted && !f.local_only)
440 .collect()
441 }
442
443 pub fn queue_submission(&mut self, fact: PersonalFact) -> Result<bool> {
445 if fact.local_only {
446 return Ok(false); }
448
449 if self.pending_submissions.len() >= self.max_queue_size {
450 return Ok(false);
451 }
452
453 let submission = PendingFactSubmission::new(fact);
454 let json = serde_json::to_string(&submission.fact)?;
455
456 let conn = self
457 .conn
458 .lock()
459 .expect("personal knowledge cache connection lock poisoned");
460 conn.execute(
461 "INSERT INTO pending_fact_submissions (fact_json, queued_at, attempts) VALUES (?1, ?2, ?3)",
462 params![json, submission.queued_at, submission.attempts],
463 )?;
464
465 self.pending_submissions.push(submission);
466 Ok(true)
467 }
468
469 pub fn pending_submissions(&self) -> &[PendingFactSubmission] {
471 &self.pending_submissions
472 }
473
474 pub fn clear_pending_submissions(&mut self) -> Result<()> {
476 self.pending_submissions.clear();
477 let conn = self
478 .conn
479 .lock()
480 .expect("personal knowledge cache connection lock poisoned");
481 conn.execute("DELETE FROM pending_fact_submissions", [])?;
482 Ok(())
483 }
484
485 pub fn queue_feedback(&mut self, feedback: PersonalFactFeedback) -> Result<bool> {
487 if let Some(fact) = self.facts.get(&feedback.fact_id)
489 && fact.local_only
490 {
491 return Ok(false); }
493
494 if self.pending_feedback.len() >= self.max_queue_size {
495 return Ok(false);
496 }
497
498 let conn = self
499 .conn
500 .lock()
501 .expect("personal knowledge cache connection lock poisoned");
502 conn.execute(
503 "INSERT INTO pending_fact_feedback (fact_id, is_reinforcement, context, timestamp)
504 VALUES (?1, ?2, ?3, ?4)",
505 params![
506 feedback.fact_id,
507 feedback.is_reinforcement as i32,
508 feedback.context,
509 feedback.timestamp,
510 ],
511 )?;
512
513 self.pending_feedback.push(feedback);
514 Ok(true)
515 }
516
517 pub fn pending_feedback(&self) -> &[PersonalFactFeedback] {
519 &self.pending_feedback
520 }
521
522 pub fn clear_pending_feedback(&mut self) -> Result<()> {
524 self.pending_feedback.clear();
525 let conn = self
526 .conn
527 .lock()
528 .expect("personal knowledge cache connection lock poisoned");
529 conn.execute("DELETE FROM pending_fact_feedback", [])?;
530 Ok(())
531 }
532
533 pub fn merge_from_server(&mut self, server_facts: Vec<PersonalFact>) -> Result<MergeResult> {
535 let mut added = 0;
536 let mut updated = 0;
537 let mut conflicts = 0;
538
539 for server_fact in server_facts {
540 if server_fact.local_only {
542 continue;
543 }
544
545 if let Some(local_fact) = self.facts.get(&server_fact.id) {
546 if server_fact.version > local_fact.version {
548 self.save_fact_to_db(&server_fact)?;
550 self.facts_by_key
551 .insert(server_fact.key.clone(), server_fact.id.clone());
552 self.facts.insert(server_fact.id.clone(), server_fact);
553 updated += 1;
554 } else if server_fact.version < local_fact.version {
555 conflicts += 1;
557 }
558 } else {
560 self.save_fact_to_db(&server_fact)?;
562 self.facts_by_key
563 .insert(server_fact.key.clone(), server_fact.id.clone());
564 self.facts.insert(server_fact.id.clone(), server_fact);
565 added += 1;
566 }
567 }
568
569 Ok(MergeResult {
570 added,
571 updated,
572 conflicts,
573 })
574 }
575
576 pub fn apply_decay(&mut self) -> Result<u32> {
578 let mut decayed = 0;
579
580 for fact in self.facts.values_mut() {
581 let old_confidence = fact.confidence;
582 fact.apply_decay();
583 if (fact.confidence - old_confidence).abs() > 0.001 {
584 decayed += 1;
585 }
586 }
587
588 if decayed > 0 {
590 for fact in self.facts.values() {
591 self.save_fact_to_db(fact)?;
592 }
593 }
594
595 Ok(decayed)
596 }
597
598 pub fn stats(&self) -> CacheStats {
600 let mut by_category: HashMap<PersonalFactCategory, u32> = HashMap::new();
601 let mut total_confidence = 0.0f32;
602 let mut count = 0u32;
603 let mut local_only_count = 0u32;
604
605 for fact in self.facts.values().filter(|f| !f.deleted) {
606 *by_category.entry(fact.category).or_insert(0) += 1;
607 total_confidence += fact.confidence;
608 count += 1;
609 if fact.local_only {
610 local_only_count += 1;
611 }
612 }
613
614 CacheStats {
615 total_facts: count,
616 by_category,
617 avg_confidence: if count > 0 {
618 total_confidence / count as f32
619 } else {
620 0.0
621 },
622 local_only_facts: local_only_count,
623 pending_submissions: self.pending_submissions.len(),
624 pending_feedback: self.pending_feedback.len(),
625 last_sync: self.last_sync,
626 }
627 }
628
629 pub fn export_json(&self) -> Result<String> {
631 let facts: Vec<&PersonalFact> = self.facts.values().filter(|f| !f.deleted).collect();
632 Ok(serde_json::to_string_pretty(&facts)?)
633 }
634
635 pub fn import_json(&mut self, json: &str) -> Result<ImportResult> {
637 let facts: Vec<PersonalFact> = serde_json::from_str(json)?;
638 let mut imported = 0;
639 let mut updated = 0;
640
641 for mut fact in facts {
642 if let Some(existing_id) = self.facts_by_key.get(&fact.key) {
643 fact.id = existing_id.clone();
645 updated += 1;
646 } else {
647 imported += 1;
648 }
649 self.upsert_fact(fact)?;
650 }
651
652 Ok(ImportResult { imported, updated })
653 }
654}
655
656#[derive(Debug, Clone)]
658pub struct MergeResult {
659 pub added: u32,
661 pub updated: u32,
663 pub conflicts: u32,
665}
666
667#[derive(Debug, Clone)]
669pub struct ImportResult {
670 pub imported: u32,
672 pub updated: u32,
674}
675
676#[derive(Debug, Clone)]
678pub struct CacheStats {
679 pub total_facts: u32,
681 pub by_category: HashMap<PersonalFactCategory, u32>,
683 pub avg_confidence: f32,
685 pub local_only_facts: u32,
687 pub pending_submissions: usize,
689 pub pending_feedback: usize,
691 pub last_sync: i64,
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::knowledge::bks_pks::personal::fact::PersonalFactSource;
699
700 fn create_test_fact(key: &str, value: &str) -> PersonalFact {
701 PersonalFact::new(
702 PersonalFactCategory::Preference,
703 key.to_string(),
704 value.to_string(),
705 None,
706 PersonalFactSource::ExplicitStatement,
707 false,
708 )
709 }
710
711 #[test]
712 fn test_cache_creation() {
713 let cache = PersonalKnowledgeCache::in_memory(100).unwrap();
714 assert_eq!(cache.last_sync, 0);
715 assert_eq!(cache.all_facts().count(), 0);
716 }
717
718 #[test]
719 fn test_add_and_get_fact() {
720 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
721 let fact = create_test_fact("language", "Rust");
722
723 let id = fact.id.clone();
724 cache.add_fact(fact).unwrap();
725
726 let retrieved = cache.get_fact(&id).unwrap();
727 assert_eq!(retrieved.value, "Rust");
728 }
729
730 #[test]
731 fn test_get_by_key() {
732 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
733 cache
734 .add_fact(create_test_fact("language", "Rust"))
735 .unwrap();
736
737 let retrieved = cache.get_fact_by_key("language").unwrap();
738 assert_eq!(retrieved.value, "Rust");
739 }
740
741 #[test]
742 fn test_upsert_fact() {
743 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
744
745 cache
747 .upsert_fact(create_test_fact("language", "Python"))
748 .unwrap();
749 assert_eq!(cache.get_fact_by_key("language").unwrap().value, "Python");
750
751 cache
753 .upsert_fact(create_test_fact("language", "Rust"))
754 .unwrap();
755 let fact = cache.get_fact_by_key("language").unwrap();
756 assert_eq!(fact.value, "Rust");
757 assert_eq!(fact.reinforcements, 1); }
759
760 #[test]
761 fn test_facts_by_category() {
762 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
763
764 cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
765
766 let mut identity_fact = create_test_fact("name", "John");
767 identity_fact.category = PersonalFactCategory::Identity;
768 cache.add_fact(identity_fact).unwrap();
769
770 let pref_facts = cache.facts_by_category(PersonalFactCategory::Preference);
771 assert_eq!(pref_facts.len(), 1);
772
773 let id_facts = cache.facts_by_category(PersonalFactCategory::Identity);
774 assert_eq!(id_facts.len(), 1);
775 }
776
777 #[test]
778 fn test_search_facts() {
779 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
780
781 cache
782 .add_fact(create_test_fact("language", "Rust"))
783 .unwrap();
784 cache
785 .add_fact(create_test_fact("framework", "Actix"))
786 .unwrap();
787
788 let results = cache.search_facts("rust");
789 assert_eq!(results.len(), 1);
790 assert_eq!(results[0].key, "language");
791 }
792
793 #[test]
794 fn test_local_only_facts() {
795 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
796
797 let mut local_fact = create_test_fact("secret", "value");
798 local_fact.local_only = true;
799 cache.add_fact(local_fact.clone()).unwrap();
800
801 assert!(!cache.queue_submission(local_fact).unwrap());
803
804 assert!(cache.get_syncable_facts().is_empty());
806 }
807
808 #[test]
809 fn test_export_import_json() {
810 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
811
812 cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
813 cache
814 .add_fact(create_test_fact("editor", "VSCode"))
815 .unwrap();
816
817 let json = cache.export_json().unwrap();
818
819 let mut new_cache = PersonalKnowledgeCache::in_memory(100).unwrap();
821 let result = new_cache.import_json(&json).unwrap();
822
823 assert_eq!(result.imported, 2);
824 assert_eq!(result.updated, 0);
825 assert_eq!(new_cache.all_facts().count(), 2);
826 }
827
828 #[test]
829 fn test_stats() {
830 let mut cache = PersonalKnowledgeCache::in_memory(100).unwrap();
831
832 cache.add_fact(create_test_fact("lang", "Rust")).unwrap();
833
834 let mut local_fact = create_test_fact("secret", "value");
835 local_fact.local_only = true;
836 cache.add_fact(local_fact).unwrap();
837
838 let stats = cache.stats();
839 assert_eq!(stats.total_facts, 2);
840 assert_eq!(stats.local_only_facts, 1);
841 }
842}