Skip to main content

deepstrike_core/memory/
curator.rs

1//! Pure record-level curation helpers.
2//!
3//! This module performs no storage mutation and owns no lifecycle state. Durable writes always
4//! travel through the kernel `WriteMemory` syscall; hosts may use these helpers inside their
5//! upsert implementation to resolve a candidate against an already-loaded snapshot.
6
7use crate::mm::memory::MemoryRecord;
8
9/// Conflict rule used by a host while resolving a fuzzy duplicate.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ConflictResolution {
12    PreferNewer,
13    PreferHigherConfidence,
14}
15
16/// Whether two records address the same durable key.
17pub fn same_scoped_key(left: &MemoryRecord, right: &MemoryRecord) -> bool {
18    left.scope == right.scope && left.kind == right.kind && left.name == right.name
19}
20
21/// Deterministic term-set Jaccard score used only after scoped-key lookup misses.
22///
23/// Tokenizes through the shared [`crate::lexical`] vocabulary (word runs + Han
24/// bigrams) rather than whitespace, so near-duplicate CJK content — which has no
25/// whitespace to split on — is actually detectable against a dedupe threshold.
26pub fn jaccard_similarity(left: &str, right: &str) -> f64 {
27    crate::lexical::jaccard(left, right)
28}
29
30/// Select the record retained for a conflict without performing any I/O.
31pub fn resolve_conflict<'a>(
32    existing: &'a MemoryRecord,
33    incoming: &'a MemoryRecord,
34    policy: ConflictResolution,
35) -> &'a MemoryRecord {
36    match policy {
37        ConflictResolution::PreferNewer => incoming,
38        ConflictResolution::PreferHigherConfidence => {
39            if incoming.confidence >= existing.confidence {
40                incoming
41            } else {
42                existing
43            }
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use crate::mm::memory::{
52        MemoryAuthor, MemoryKind, MemoryProvenance, MemoryScope, MemoryTrustLevel,
53    };
54
55    fn record(name: &str, content: &str, confidence: f64) -> MemoryRecord {
56        MemoryRecord {
57            record_id: format!("record-{name}"),
58            scope: MemoryScope::new("tenant", "project"),
59            name: name.into(),
60            kind: MemoryKind::Project,
61            content: content.into(),
62            description: String::new(),
63            provenance: MemoryProvenance {
64                session_id: Some("session".into()),
65                author: MemoryAuthor::Extraction,
66                trust: MemoryTrustLevel::Untrusted,
67                evidence_refs: Vec::new(),
68            },
69            created_at: 1,
70            updated_at: 1,
71            last_recalled_at: None,
72            recall_count: 0,
73            confidence,
74            links: Vec::new(),
75            pinned: false,
76            ttl_days: None,
77        }
78    }
79
80    #[test]
81    fn scoped_key_precedes_content_similarity() {
82        let existing = record("compiler", "prefer cargo nextest", 0.8);
83        let same_key = record("compiler", "use cargo test", 0.7);
84        let other_key = record("editor", "prefer cargo nextest", 0.9);
85        assert!(same_scoped_key(&existing, &same_key));
86        assert!(!same_scoped_key(&existing, &other_key));
87        assert_eq!(
88            jaccard_similarity(&existing.content, &other_key.content),
89            1.0
90        );
91    }
92
93    #[test]
94    fn cjk_near_duplicates_are_detectable_after_key_miss() {
95        // Whitespace tokenization turned a whole Chinese sentence into one token, so
96        // near-duplicates scored ~0 and no dedupe threshold could ever fire on them.
97        let near = jaccard_similarity("用户偏好深色模式界面", "用户偏好浅色模式界面");
98        let unrelated = jaccard_similarity("用户偏好深色模式界面", "周五之前完成部署上线");
99        assert!(near > 0.5, "near-duplicate CJK must be detectable: {near}");
100        assert!(unrelated < 0.2, "unrelated CJK must stay low: {unrelated}");
101    }
102
103    #[test]
104    fn conflict_resolution_is_pure_and_deterministic() {
105        let old = record("compiler", "old", 0.9);
106        let new = record("compiler", "new", 0.7);
107        assert_eq!(
108            resolve_conflict(&old, &new, ConflictResolution::PreferNewer).content,
109            "new"
110        );
111        assert_eq!(
112            resolve_conflict(&old, &new, ConflictResolution::PreferHigherConfidence).content,
113            "old"
114        );
115    }
116}