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 std::collections::HashSet;
8
9use crate::mm::memory::MemoryRecord;
10
11/// Conflict rule used by a host while resolving a fuzzy duplicate.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ConflictResolution {
14    PreferNewer,
15    PreferHigherConfidence,
16}
17
18/// Whether two records address the same durable key.
19pub fn same_scoped_key(left: &MemoryRecord, right: &MemoryRecord) -> bool {
20    left.scope == right.scope && left.kind == right.kind && left.name == right.name
21}
22
23/// Deterministic token-set Jaccard score used only after scoped-key lookup misses.
24pub fn jaccard_similarity(left: &str, right: &str) -> f64 {
25    let left: HashSet<&str> = left.split_whitespace().collect();
26    let right: HashSet<&str> = right.split_whitespace().collect();
27    let intersection = left.intersection(&right).count();
28    let union = left.union(&right).count();
29    if union == 0 {
30        0.0
31    } else {
32        intersection as f64 / union as f64
33    }
34}
35
36/// Select the record retained for a conflict without performing any I/O.
37pub fn resolve_conflict<'a>(
38    existing: &'a MemoryRecord,
39    incoming: &'a MemoryRecord,
40    policy: ConflictResolution,
41) -> &'a MemoryRecord {
42    match policy {
43        ConflictResolution::PreferNewer => incoming,
44        ConflictResolution::PreferHigherConfidence => {
45            if incoming.confidence >= existing.confidence {
46                incoming
47            } else {
48                existing
49            }
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::mm::memory::{
58        MemoryAuthor, MemoryKind, MemoryProvenance, MemoryScope, MemoryTrustLevel,
59    };
60
61    fn record(name: &str, content: &str, confidence: f64) -> MemoryRecord {
62        MemoryRecord {
63            record_id: format!("record-{name}"),
64            scope: MemoryScope::new("tenant", "project"),
65            name: name.into(),
66            kind: MemoryKind::Project,
67            content: content.into(),
68            description: String::new(),
69            provenance: MemoryProvenance {
70                session_id: Some("session".into()),
71                author: MemoryAuthor::Extraction,
72                trust: MemoryTrustLevel::Untrusted,
73                evidence_refs: Vec::new(),
74            },
75            created_at: 1,
76            updated_at: 1,
77            last_recalled_at: None,
78            recall_count: 0,
79            confidence,
80            links: Vec::new(),
81            pinned: false,
82            ttl_days: None,
83        }
84    }
85
86    #[test]
87    fn scoped_key_precedes_content_similarity() {
88        let existing = record("compiler", "prefer cargo nextest", 0.8);
89        let same_key = record("compiler", "use cargo test", 0.7);
90        let other_key = record("editor", "prefer cargo nextest", 0.9);
91        assert!(same_scoped_key(&existing, &same_key));
92        assert!(!same_scoped_key(&existing, &other_key));
93        assert_eq!(
94            jaccard_similarity(&existing.content, &other_key.content),
95            1.0
96        );
97    }
98
99    #[test]
100    fn conflict_resolution_is_pure_and_deterministic() {
101        let old = record("compiler", "old", 0.9);
102        let new = record("compiler", "new", 0.7);
103        assert_eq!(
104            resolve_conflict(&old, &new, ConflictResolution::PreferNewer).content,
105            "new"
106        );
107        assert_eq!(
108            resolve_conflict(&old, &new, ConflictResolution::PreferHigherConfidence).content,
109            "old"
110        );
111    }
112}