Skip to main content

dhive_core/
curation.rs

1//! 策展协议 — 记忆质量评分公式和动作规则
2//!
3//! score = weight × confidenceFactor × freshness × textQuality × (1 + usageBonus) − disputeDecay + levelBonus
4
5use crate::memory::Mem;
6
7/// 策展动作
8#[derive(Debug, PartialEq, Eq)]
9pub enum CurateAction {
10    /// 删除极低分记忆
11    Delete,
12    /// 保留 (不改动)
13    Keep,
14    /// 提议合并
15    Merge,
16    /// 提议升级 (activity → fact → cornerstone)
17    Promote,
18    /// 需要人工审核
19    Review,
20    /// 基石过期
21    Stale,
22    /// 降级
23    Demote,
24}
25
26/// 策展建议
27#[derive(Debug)]
28pub struct CurateRecommendation {
29    pub action: CurateAction,
30    pub memory_id: String,
31    pub score: f64,
32    pub reason: String,
33    pub automatable: bool,
34}
35
36/// 计算完整策展评分
37///
38/// 公式:
39/// score = weight × confidenceFactor × freshness × textQuality × (1 + usageBonus) − disputeDecay + levelBonus
40pub fn calculate_score(mem: &Mem, days_since_update: u32) -> f64 {
41    let confidence_factor = mem.trust.as_ref()
42        .map(|t| t.confidence.factor())
43        .unwrap_or(0.4); // default: uncertain
44
45    let freshness = (1.0 - days_since_update as f64 / 180.0).max(0.0);
46
47    let text_quality = assess_text_quality(&mem.content);
48
49    let usage_bonus = {
50        let loaded = mem.loaded_count as f64;
51        let referenced = mem.referenced_count as f64;
52        (loaded * 0.01 + referenced * 0.02).min(0.5)
53    };
54
55    let dispute_decay = mem.trust.as_ref()
56        .map(|t| t.dispute_decay())
57        .unwrap_or(0.0);
58
59    let level_bonus = match mem.level {
60        crate::memory::MemLevel::Cornerstone => 0.2,
61        crate::memory::MemLevel::Fact => 0.0,
62        crate::memory::MemLevel::Activity => -0.1,
63    };
64
65    mem.weight * confidence_factor * freshness * text_quality * (1.0 + usage_bonus) - dispute_decay + level_bonus
66}
67
68/// 评估文本质量 (0.0–1.0)
69///
70/// 6 个维度启发式评估
71pub fn assess_text_quality(content: &str) -> f64 {
72    let len = content.chars().count();
73
74    // 1. 内容长度 (权重 0.15)
75    let len_score = if len < 20 { 0.2 } else if len >= 100 { 1.0 } else { (len as f64 - 20.0) / 80.0 * 0.8 + 0.2 };
76
77    // 2. 技术关键词密度 (权重 0.20)
78    let tech_keywords = [
79        "类型", "函数", "模块", "架构", "测试", "API", "接口", "协议",
80        "引擎", "算法", "数据", "状态", "搜索", "索引", "配置",
81        "type", "fn", "mod", "struct", "impl", "async", "trait",
82        "test", "api", "http", "json", "sql", "db",
83    ];
84    let tech_count = tech_keywords.iter().filter(|k| content.to_lowercase().contains(&k.to_lowercase())).count();
85    let tech_score = (tech_count as f64 / 5.0).min(1.0);
86
87    // 3. 因果/方法论结构 (权重 0.25)
88    let causal_markers = ["→", "->", "因为", "所以", "因此", "问题", "解决", "导致", "原因是", "方案", "because", "therefore", "solution"];
89    let causal_count = causal_markers.iter().filter(|m| content.to_lowercase().contains(&m.to_lowercase())).count();
90    let causal_score = (causal_count as f64 / 3.0).min(1.0);
91
92    // 4. 重复字符检测 (权重 0.10) — 垃圾内容特征
93    let rep_score = if has_repetition(content, 8) { 0.0 } else { 1.0 };
94
95    // 5. 虚词占比 (权重 0.15) — 过高的虚词占比扣分
96    let filler_ratio = filler_word_ratio(content);
97    let filler_score = 1.0 - filler_ratio.min(1.0);
98
99    // 6. 量化数据信号 (权重 0.15)
100    let quant_count = content.chars().filter(|c| c.is_ascii_digit()).count();
101    let quant_score = (quant_count as f64 / 10.0).min(1.0);
102
103    len_score * 0.15 + tech_score * 0.20 + causal_score * 0.25 + rep_score * 0.10 + filler_score * 0.15 + quant_score * 0.15
104}
105
106/// 检测连续重复字符
107fn has_repetition(content: &str, threshold: usize) -> bool {
108    let chars: Vec<char> = content.chars().collect();
109    if chars.len() < threshold { return false; }
110    for i in 0..=chars.len() - threshold {
111        if chars[i..i + threshold].iter().all(|&c| c == chars[i]) {
112            return true;
113        }
114    }
115    false
116}
117
118/// 计算虚词占比
119fn filler_word_ratio(content: &str) -> f64 {
120    let fillers = ["的", "了", "是", "在", "和", "有", "个", "也", "就", "都", "the", "a", "an", "is", "of", "to", "in", "and", "or", "it"];
121    let chars: Vec<char> = content.chars().collect();
122    if chars.is_empty() { return 0.0; }
123    // 简化:按字符匹配
124    let filler_chars: std::collections::HashSet<char> = fillers.iter()
125        .flat_map(|s| s.chars())
126        .collect();
127    let filler_count = chars.iter().filter(|c| filler_chars.contains(c)).count();
128    filler_count as f64 / chars.len() as f64
129}
130
131/// 根据评分确定策展动作
132pub fn determine_action(mem: &Mem, score: f64) -> CurateRecommendation {
133    let is_cornerstone = mem.level == crate::memory::MemLevel::Cornerstone;
134    let is_activity = mem.level == crate::memory::MemLevel::Activity;
135    let has_disputes = mem.trust.as_ref()
136        .map(|t| t.disputes.as_ref().map(|d| !d.is_empty()).unwrap_or(false))
137        .unwrap_or(false);
138
139    let (action, reason, automatable) = if score < 0.15 && is_activity && !is_cornerstone {
140        (CurateAction::Delete, "极低评分 + 临时活动记忆".into(), true)
141    } else if is_cornerstone && has_disputes {
142        (CurateAction::Demote, "基石记忆存在未解决的争议".into(), false)
143    } else if is_cornerstone && score < 0.3 {
144        (CurateAction::Stale, "基石记忆长期未更新,评分过低".into(), false)
145    } else if score > 0.6 && mem.weight >= 0.7 && !has_disputes && !is_cornerstone {
146        (CurateAction::Promote, "高质量记忆,建议升级级别".into(), false)
147    } else if score < 0.3 && !is_activity {
148        (CurateAction::Review, "评分偏低,需要人工审核".into(), false)
149    } else {
150        (CurateAction::Keep, "评分正常,保留".into(), true)
151    };
152
153    CurateRecommendation {
154        action,
155        memory_id: mem.id.clone(),
156        score,
157        reason,
158        automatable,
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::memory::{Mem, MemLevel, MemoryZone};
166    use crate::trust::Confidence;
167
168    fn make_cur_mem(id: &str, content: &str, level: MemLevel, weight: f64, confidence: Confidence, loaded: u32, referenced: u32, disputes: u32) -> Mem {
169        let trust = {
170            let mut td = crate::trust::TrustData::new(confidence, "test");
171            if disputes > 0 {
172                let d: Vec<crate::trust::Dispute> = (0..disputes).map(|i| crate::trust::Dispute {
173                    id: format!("d{}", i),
174                    reason: "测试争议".into(),
175                    disputed_at: "2026-01-01T00:00:00Z".into(),
176                    resolved_at: None,
177                    resolution: None,
178                }).collect();
179                td.disputes = Some(d);
180            }
181            Some(td)
182        };
183
184        Mem {
185            id: id.into(),
186            level,
187            zone: MemoryZone::General,
188            content: content.into(),
189            weight,
190            tags: vec![],
191            source: None, metadata: None,
192            last_matched: None, match_count: 0,
193            loaded_count: loaded, referenced_count: referenced,
194            last_loaded: None, last_referenced: None,
195            supersedes: None, superseded_at: None, superseded_by: None,
196            trust,
197            created_at: "2026-01-01T00:00:00Z".into(),
198            updated_at: "2026-01-01T00:00:00Z".into(),
199        }
200    }
201
202    #[test]
203    fn test_high_quality_gets_promote() {
204        let mem = make_cur_mem("m1", "基石原则:最少依赖 — 新增依赖前思考是否真的需要。核心模块架构设计应遵循类型系统约束,API 接口需要完整的测试覆盖。问题解决方案:优先使用标准库函数实现,避免引入外部依赖。根据 85% 测试覆盖率目标验证模块正确性。", MemLevel::Fact, 0.85, Confidence::High, 50, 30, 0);
205        let score = calculate_score(&mem, 5);
206        let rec = determine_action(&mem, score);
207        assert!(matches!(rec.action, CurateAction::Promote), "action = {:?}, score = {}", rec.action, score);
208    }
209
210    #[test]
211    fn test_low_score_activity_gets_delete() {
212        let mem = make_cur_mem("m1", "test", MemLevel::Activity, 0.1, Confidence::Speculative, 0, 0, 0);
213        let score = calculate_score(&mem, 90);
214        let rec = determine_action(&mem, score);
215        assert!(matches!(rec.action, CurateAction::Delete), "action = {:?}, score = {}", rec.action, score);
216        assert!(rec.automatable);
217    }
218
219    #[test]
220    fn test_cornerstone_with_disputes_gets_demote() {
221        let mem = make_cur_mem("cs1", "基石原则", MemLevel::Cornerstone, 0.9, Confidence::Certain, 100, 50, 2);
222        let score = calculate_score(&mem, 10);
223        let rec = determine_action(&mem, score);
224        assert!(matches!(rec.action, CurateAction::Demote), "action = {:?}, score = {}", rec.action, score);
225        assert!(!rec.automatable);
226    }
227
228    #[test]
229    fn test_text_quality_range() {
230        let high = assess_text_quality("基石原则:最少依赖 — 新增依赖前思考是否真的需要。类型系统架构设计需要完整的模块 API 接口测试,解决方案应优先使用标准库函数。目标:85% 测试覆盖率,确保代码质量达到行业标准。");
231        let low = assess_text_quality("test");
232        assert!(high > low, "high={}, low={}", high, low);
233        assert!(high > 0.5);
234        assert!(low < 0.5);
235    }
236
237    #[test]
238    fn test_freshness_decay() {
239        let mem = make_cur_mem("m1", "测试记忆 — 包含技术关键词 TypeScript 类型系统 架构设计", MemLevel::Fact, 0.5, Confidence::Medium, 0, 0, 0);
240        let score_recent = calculate_score(&mem, 1);
241        let score_old = calculate_score(&mem, 200);
242        assert!(score_recent > score_old, "recent={}, old={}", score_recent, score_old);
243    }
244}