dhive-core 0.1.0

D-HIVE Trust Protocol — Rust core: BM25 search, LCS similarity, canonicalHash, pack format, curation scoring
Documentation
//! Pack 格式 — .hive-pack.json 解析/验证/生成
//!
//! D-HIVE 记忆包交换格式。支持导入、导出、验证、冲突检测。

use serde::{Deserialize, Serialize};
use crate::similarity::{lcs_similarity, canonical_hash, ConflictCategory};
use crate::memory::{Mem, MemLevel, MemoryZone};

/// Pack 元数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackMeta {
    /// 包名 (kebab-case)
    pub name: String,
    /// 语义化版本
    pub version: String,
    /// 作者标识
    #[serde(skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,
    /// 1-3 句话描述
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// 创建此包的 D-HIVE 版本
    #[serde(skip_serializing_if = "Option::is_none", rename = "dhiveVersion")]
    pub dhive_version: Option<String>,
    /// 包级别标签
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tags: Option<Vec<String>>,
    /// 内含记忆条数 (导出工具自动填写)
    #[serde(skip_serializing_if = "Option::is_none", rename = "memoryCount")]
    pub memory_count: Option<u32>,
}

/// 导出用的精简版 Memory 条目 (不含运行时字段)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackMem {
    pub content: String,
    pub level: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub zone: Option<String>,
    pub weight: f64,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trust: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

/// .hive-pack.json 完整结构
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HivePack {
    pub pack: PackMeta,
    #[serde(default)]
    pub memories: Vec<PackMem>,
}

/// 验证结果
#[derive(Debug)]
pub struct ValidationResult {
    pub valid: bool,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
    pub memory_count: usize,
}

/// 验证 pack 文件
pub fn validate_pack(pack: &HivePack) -> ValidationResult {
    let mut errors = vec![];
    let mut warnings = vec![];

    // pack name validation
    if pack.pack.name.is_empty() {
        errors.push("pack.name 缺失".into());
    } else if !is_kebab_case(&pack.pack.name) {
        warnings.push("pack.name 建议使用 kebab-case 格式".into());
    }

    // pack version
    if pack.pack.version.is_empty() {
        errors.push("pack.version 缺失".into());
    } else if !is_semver(&pack.pack.version) {
        warnings.push(format!("pack.version '{}' 不是标准 semver 格式", pack.pack.version));
    }

    // memories validation
    if pack.memories.is_empty() {
        warnings.push("memories 为空".into());
    }
    if pack.memories.len() > 200 {
        errors.push(format!("memories 超过 200 条上限 (当前: {})", pack.memories.len()));
    }

    for (i, m) in pack.memories.iter().enumerate() {
        if m.content.trim().is_empty() {
            errors.push(format!("memories[{}].content 不能为空", i));
        }

        if MemLevel::from_str(&m.level).is_none() {
            errors.push(format!("memories[{}].level 无效: {}", i, m.level));
        }

        if let Some(ref zone) = m.zone {
            if MemoryZone::from_str(zone).is_none() {
                errors.push(format!("memories[{}].zone 无效: {}", i, zone));
            }
        }

        if let Some(ref trust) = m.trust {
            if let Some(conf) = trust.get("confidence") {
                if let Some(c) = conf.as_str() {
                    if crate::trust::Confidence::from_str(c).is_none() {
                        errors.push(format!("memories[{}].trust.confidence 无效: {}", i, c));
                    }
                }
            }
        }
    }

    ValidationResult {
        valid: errors.is_empty(),
        errors,
        warnings,
        memory_count: pack.memories.len(),
    }
}

/// 冲突检测:检查一条 incoming memory 与 existing 列表的冲突
#[derive(Debug)]
pub struct ConflictResult {
    pub status: String,
    pub canonical_match: bool,
    pub best_similarity: f64,
    pub category: ConflictCategory,
}

pub fn detect_conflict(incoming: &PackMem, existing: &[&Mem]) -> ConflictResult {
    let inc_hash = canonical_hash(&incoming.content);

    // 1. 精确 canonicalHash 匹配
    for m in existing {
        if let Some(ref meta) = m.metadata {
            if let Some(ex_hash) = meta.get("canonicalHash").and_then(|v| v.as_str()) {
                if ex_hash == inc_hash {
                    return ConflictResult {
                        status: "exact-duplicate".into(),
                        canonical_match: true,
                        best_similarity: 1.0,
                        category: ConflictCategory::Exact,
                    };
                }
            }
        }
    }

    // 2. LCS 相似度最高匹配
    let mut best_sim: f64 = 0.0;
    for m in existing {
        let sim = lcs_similarity(&incoming.content, &m.content);
        best_sim = best_sim.max(sim);
    }

    let category = ConflictCategory::from_similarity(best_sim);
    let status = match category {
        ConflictCategory::Exact => "exact",
        ConflictCategory::Similar => "similar",
        ConflictCategory::Related => "related",
        ConflictCategory::Unrelated => "unrelated",
    };

    ConflictResult {
        status: status.into(),
        canonical_match: false,
        best_similarity: best_sim,
        category,
    }
}

fn is_kebab_case(s: &str) -> bool {
    if s.is_empty() { return false; }
    s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
        && !s.starts_with('-')
        && !s.ends_with('-')
}

fn is_semver(s: &str) -> bool {
    let parts: Vec<&str> = s.split('.').collect();
    if parts.len() != 3 { return false; }
    parts.iter().all(|p| p.parse::<u32>().is_ok())
}

/// 从 HivePack 创建导入用的记忆 (用于导入后填充运行时字段)
pub fn create_mem_from_pack(pm: &PackMem, id: &str, now: &str, source_pack: &str) -> Mem {
    let trust = pm.trust.as_ref().map(|t| {
        serde_json::from_value(t.clone()).unwrap_or_else(|_| {
            crate::trust::TrustData::new(
                crate::trust::Confidence::Uncertain,
                "pack-import",
            )
        })
    });

    let mut metadata = pm.metadata.clone().unwrap_or(serde_json::Value::Object(Default::default()));
    if let Some(obj) = metadata.as_object_mut() {
        obj.insert("sourcePack".into(), serde_json::Value::String(source_pack.into()));
        obj.insert("importedAt".into(), serde_json::Value::String(now.into()));
        if !obj.contains_key("canonicalHash") {
            obj.insert("canonicalHash".into(), serde_json::Value::String(canonical_hash(&pm.content)));
        }
    }

    Mem {
        id: id.into(),
        level: MemLevel::from_str(&pm.level).unwrap_or(MemLevel::Fact),
        zone: pm.zone.as_ref()
            .and_then(|z| MemoryZone::from_str(z))
            .unwrap_or_default(),
        content: pm.content.clone(),
        weight: pm.weight,
        tags: pm.tags.clone(),
        source: Some("pack-import".into()),
        metadata: Some(metadata),
        last_matched: None,
        match_count: 0,
        loaded_count: 0,
        referenced_count: 0,
        last_loaded: None,
        last_referenced: None,
        supersedes: None,
        superseded_at: None,
        superseded_by: None,
        trust,
        created_at: now.into(),
        updated_at: now.into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_valid_pack() {
        let pack = HivePack {
            pack: PackMeta {
                name: "test-pack".into(),
                version: "1.0.0".into(),
                author: None,
                description: None,
                dhive_version: None,
                tags: None,
                memory_count: None,
            },
            memories: vec![
                PackMem {
                    content: "测试记忆".into(),
                    level: "fact".into(),
                    zone: None,
                    weight: 0.5,
                    tags: vec![],
                    trust: None,
                    metadata: None,
                }
            ],
        };
        let result = validate_pack(&pack);
        assert!(result.valid);
        assert_eq!(result.memory_count, 1);
    }

    #[test]
    fn test_validate_empty_name() {
        let pack = HivePack {
            pack: PackMeta {
                name: "".into(),
                version: "1.0.0".into(),
                author: None, description: None,
                dhive_version: None, tags: None, memory_count: None,
            },
            memories: vec![],
        };
        let result = validate_pack(&pack);
        assert!(!result.valid);
        assert!(result.errors.iter().any(|e| e.contains("name")));
    }

    #[test]
    fn test_validate_empty_content() {
        let pack = HivePack {
            pack: PackMeta {
                name: "test".into(), version: "1.0.0".into(),
                author: None, description: None,
                dhive_version: None, tags: None, memory_count: None,
            },
            memories: vec![
                PackMem {
                    content: "".into(),
                    level: "fact".into(),
                    zone: None, weight: 0.5, tags: vec![],
                    trust: None, metadata: None,
                }
            ],
        };
        let result = validate_pack(&pack);
        assert!(!result.valid);
        assert!(result.errors.iter().any(|e| e.contains("content")));
    }

    #[test]
    fn test_validate_invalid_level() {
        let pack = HivePack {
            pack: PackMeta {
                name: "test".into(), version: "1.0.0".into(),
                author: None, description: None,
                dhive_version: None, tags: None, memory_count: None,
            },
            memories: vec![
                PackMem {
                    content: "test".into(),
                    level: "invalid".into(),
                    zone: None, weight: 0.5, tags: vec![],
                    trust: None, metadata: None,
                }
            ],
        };
        let result = validate_pack(&pack);
        assert!(!result.valid);
    }

    #[test]
    fn test_semver() {
        assert!(is_semver("1.0.0"));
        assert!(is_semver("0.3.0"));
        assert!(!is_semver("v1.0"));
        assert!(!is_semver("1.0"));
    }

    #[test]
    fn test_kebab_case() {
        assert!(is_kebab_case("typescript-core"));
        assert!(is_kebab_case("test-pack-2"));
        assert!(!is_kebab_case("INVALID"));
        assert!(!is_kebab_case("-bad"));
    }
}