dhive-core 0.1.0

D-HIVE Trust Protocol — Rust core: BM25 search, LCS similarity, canonicalHash, pack format, curation scoring
Documentation
//! 策展协议 — 记忆质量评分公式和动作规则
//!
//! score = weight × confidenceFactor × freshness × textQuality × (1 + usageBonus) − disputeDecay + levelBonus

use crate::memory::Mem;

/// 策展动作
#[derive(Debug, PartialEq, Eq)]
pub enum CurateAction {
    /// 删除极低分记忆
    Delete,
    /// 保留 (不改动)
    Keep,
    /// 提议合并
    Merge,
    /// 提议升级 (activity → fact → cornerstone)
    Promote,
    /// 需要人工审核
    Review,
    /// 基石过期
    Stale,
    /// 降级
    Demote,
}

/// 策展建议
#[derive(Debug)]
pub struct CurateRecommendation {
    pub action: CurateAction,
    pub memory_id: String,
    pub score: f64,
    pub reason: String,
    pub automatable: bool,
}

/// 计算完整策展评分
///
/// 公式:
/// score = weight × confidenceFactor × freshness × textQuality × (1 + usageBonus) − disputeDecay + levelBonus
pub fn calculate_score(mem: &Mem, days_since_update: u32) -> f64 {
    let confidence_factor = mem.trust.as_ref()
        .map(|t| t.confidence.factor())
        .unwrap_or(0.4); // default: uncertain

    let freshness = (1.0 - days_since_update as f64 / 180.0).max(0.0);

    let text_quality = assess_text_quality(&mem.content);

    let usage_bonus = {
        let loaded = mem.loaded_count as f64;
        let referenced = mem.referenced_count as f64;
        (loaded * 0.01 + referenced * 0.02).min(0.5)
    };

    let dispute_decay = mem.trust.as_ref()
        .map(|t| t.dispute_decay())
        .unwrap_or(0.0);

    let level_bonus = match mem.level {
        crate::memory::MemLevel::Cornerstone => 0.2,
        crate::memory::MemLevel::Fact => 0.0,
        crate::memory::MemLevel::Activity => -0.1,
    };

    mem.weight * confidence_factor * freshness * text_quality * (1.0 + usage_bonus) - dispute_decay + level_bonus
}

/// 评估文本质量 (0.0–1.0)
///
/// 6 个维度启发式评估
pub fn assess_text_quality(content: &str) -> f64 {
    let len = content.chars().count();

    // 1. 内容长度 (权重 0.15)
    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 };

    // 2. 技术关键词密度 (权重 0.20)
    let tech_keywords = [
        "类型", "函数", "模块", "架构", "测试", "API", "接口", "协议",
        "引擎", "算法", "数据", "状态", "搜索", "索引", "配置",
        "type", "fn", "mod", "struct", "impl", "async", "trait",
        "test", "api", "http", "json", "sql", "db",
    ];
    let tech_count = tech_keywords.iter().filter(|k| content.to_lowercase().contains(&k.to_lowercase())).count();
    let tech_score = (tech_count as f64 / 5.0).min(1.0);

    // 3. 因果/方法论结构 (权重 0.25)
    let causal_markers = ["", "->", "因为", "所以", "因此", "问题", "解决", "导致", "原因是", "方案", "because", "therefore", "solution"];
    let causal_count = causal_markers.iter().filter(|m| content.to_lowercase().contains(&m.to_lowercase())).count();
    let causal_score = (causal_count as f64 / 3.0).min(1.0);

    // 4. 重复字符检测 (权重 0.10) — 垃圾内容特征
    let rep_score = if has_repetition(content, 8) { 0.0 } else { 1.0 };

    // 5. 虚词占比 (权重 0.15) — 过高的虚词占比扣分
    let filler_ratio = filler_word_ratio(content);
    let filler_score = 1.0 - filler_ratio.min(1.0);

    // 6. 量化数据信号 (权重 0.15)
    let quant_count = content.chars().filter(|c| c.is_ascii_digit()).count();
    let quant_score = (quant_count as f64 / 10.0).min(1.0);

    len_score * 0.15 + tech_score * 0.20 + causal_score * 0.25 + rep_score * 0.10 + filler_score * 0.15 + quant_score * 0.15
}

/// 检测连续重复字符
fn has_repetition(content: &str, threshold: usize) -> bool {
    let chars: Vec<char> = content.chars().collect();
    if chars.len() < threshold { return false; }
    for i in 0..=chars.len() - threshold {
        if chars[i..i + threshold].iter().all(|&c| c == chars[i]) {
            return true;
        }
    }
    false
}

/// 计算虚词占比
fn filler_word_ratio(content: &str) -> f64 {
    let fillers = ["", "", "", "", "", "", "", "", "", "", "the", "a", "an", "is", "of", "to", "in", "and", "or", "it"];
    let chars: Vec<char> = content.chars().collect();
    if chars.is_empty() { return 0.0; }
    // 简化:按字符匹配
    let filler_chars: std::collections::HashSet<char> = fillers.iter()
        .flat_map(|s| s.chars())
        .collect();
    let filler_count = chars.iter().filter(|c| filler_chars.contains(c)).count();
    filler_count as f64 / chars.len() as f64
}

/// 根据评分确定策展动作
pub fn determine_action(mem: &Mem, score: f64) -> CurateRecommendation {
    let is_cornerstone = mem.level == crate::memory::MemLevel::Cornerstone;
    let is_activity = mem.level == crate::memory::MemLevel::Activity;
    let has_disputes = mem.trust.as_ref()
        .map(|t| t.disputes.as_ref().map(|d| !d.is_empty()).unwrap_or(false))
        .unwrap_or(false);

    let (action, reason, automatable) = if score < 0.15 && is_activity && !is_cornerstone {
        (CurateAction::Delete, "极低评分 + 临时活动记忆".into(), true)
    } else if is_cornerstone && has_disputes {
        (CurateAction::Demote, "基石记忆存在未解决的争议".into(), false)
    } else if is_cornerstone && score < 0.3 {
        (CurateAction::Stale, "基石记忆长期未更新,评分过低".into(), false)
    } else if score > 0.6 && mem.weight >= 0.7 && !has_disputes && !is_cornerstone {
        (CurateAction::Promote, "高质量记忆,建议升级级别".into(), false)
    } else if score < 0.3 && !is_activity {
        (CurateAction::Review, "评分偏低,需要人工审核".into(), false)
    } else {
        (CurateAction::Keep, "评分正常,保留".into(), true)
    };

    CurateRecommendation {
        action,
        memory_id: mem.id.clone(),
        score,
        reason,
        automatable,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::memory::{Mem, MemLevel, MemoryZone};
    use crate::trust::Confidence;

    fn make_cur_mem(id: &str, content: &str, level: MemLevel, weight: f64, confidence: Confidence, loaded: u32, referenced: u32, disputes: u32) -> Mem {
        let trust = {
            let mut td = crate::trust::TrustData::new(confidence, "test");
            if disputes > 0 {
                let d: Vec<crate::trust::Dispute> = (0..disputes).map(|i| crate::trust::Dispute {
                    id: format!("d{}", i),
                    reason: "测试争议".into(),
                    disputed_at: "2026-01-01T00:00:00Z".into(),
                    resolved_at: None,
                    resolution: None,
                }).collect();
                td.disputes = Some(d);
            }
            Some(td)
        };

        Mem {
            id: id.into(),
            level,
            zone: MemoryZone::General,
            content: content.into(),
            weight,
            tags: vec![],
            source: None, metadata: None,
            last_matched: None, match_count: 0,
            loaded_count: loaded, referenced_count: referenced,
            last_loaded: None, last_referenced: None,
            supersedes: None, superseded_at: None, superseded_by: None,
            trust,
            created_at: "2026-01-01T00:00:00Z".into(),
            updated_at: "2026-01-01T00:00:00Z".into(),
        }
    }

    #[test]
    fn test_high_quality_gets_promote() {
        let mem = make_cur_mem("m1", "基石原则:最少依赖 — 新增依赖前思考是否真的需要。核心模块架构设计应遵循类型系统约束,API 接口需要完整的测试覆盖。问题解决方案:优先使用标准库函数实现,避免引入外部依赖。根据 85% 测试覆盖率目标验证模块正确性。", MemLevel::Fact, 0.85, Confidence::High, 50, 30, 0);
        let score = calculate_score(&mem, 5);
        let rec = determine_action(&mem, score);
        assert!(matches!(rec.action, CurateAction::Promote), "action = {:?}, score = {}", rec.action, score);
    }

    #[test]
    fn test_low_score_activity_gets_delete() {
        let mem = make_cur_mem("m1", "test", MemLevel::Activity, 0.1, Confidence::Speculative, 0, 0, 0);
        let score = calculate_score(&mem, 90);
        let rec = determine_action(&mem, score);
        assert!(matches!(rec.action, CurateAction::Delete), "action = {:?}, score = {}", rec.action, score);
        assert!(rec.automatable);
    }

    #[test]
    fn test_cornerstone_with_disputes_gets_demote() {
        let mem = make_cur_mem("cs1", "基石原则", MemLevel::Cornerstone, 0.9, Confidence::Certain, 100, 50, 2);
        let score = calculate_score(&mem, 10);
        let rec = determine_action(&mem, score);
        assert!(matches!(rec.action, CurateAction::Demote), "action = {:?}, score = {}", rec.action, score);
        assert!(!rec.automatable);
    }

    #[test]
    fn test_text_quality_range() {
        let high = assess_text_quality("基石原则:最少依赖 — 新增依赖前思考是否真的需要。类型系统架构设计需要完整的模块 API 接口测试,解决方案应优先使用标准库函数。目标:85% 测试覆盖率,确保代码质量达到行业标准。");
        let low = assess_text_quality("test");
        assert!(high > low, "high={}, low={}", high, low);
        assert!(high > 0.5);
        assert!(low < 0.5);
    }

    #[test]
    fn test_freshness_decay() {
        let mem = make_cur_mem("m1", "测试记忆 — 包含技术关键词 TypeScript 类型系统 架构设计", MemLevel::Fact, 0.5, Confidence::Medium, 0, 0, 0);
        let score_recent = calculate_score(&mem, 1);
        let score_old = calculate_score(&mem, 200);
        assert!(score_recent > score_old, "recent={}, old={}", score_recent, score_old);
    }
}