Skip to main content

dhive_core/
pack.rs

1//! Pack 格式 — .hive-pack.json 解析/验证/生成
2//!
3//! D-HIVE 记忆包交换格式。支持导入、导出、验证、冲突检测。
4
5use serde::{Deserialize, Serialize};
6use crate::similarity::{lcs_similarity, canonical_hash, ConflictCategory};
7use crate::memory::{Mem, MemLevel, MemoryZone};
8
9/// Pack 元数据
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PackMeta {
12    /// 包名 (kebab-case)
13    pub name: String,
14    /// 语义化版本
15    pub version: String,
16    /// 作者标识
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub author: Option<String>,
19    /// 1-3 句话描述
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub description: Option<String>,
22    /// 创建此包的 D-HIVE 版本
23    #[serde(skip_serializing_if = "Option::is_none", rename = "dhiveVersion")]
24    pub dhive_version: Option<String>,
25    /// 包级别标签
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub tags: Option<Vec<String>>,
28    /// 内含记忆条数 (导出工具自动填写)
29    #[serde(skip_serializing_if = "Option::is_none", rename = "memoryCount")]
30    pub memory_count: Option<u32>,
31}
32
33/// 导出用的精简版 Memory 条目 (不含运行时字段)
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PackMem {
36    pub content: String,
37    pub level: String,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub zone: Option<String>,
40    pub weight: f64,
41    #[serde(default)]
42    pub tags: Vec<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub trust: Option<serde_json::Value>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub metadata: Option<serde_json::Value>,
47}
48
49/// .hive-pack.json 完整结构
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HivePack {
52    pub pack: PackMeta,
53    #[serde(default)]
54    pub memories: Vec<PackMem>,
55}
56
57/// 验证结果
58#[derive(Debug)]
59pub struct ValidationResult {
60    pub valid: bool,
61    pub errors: Vec<String>,
62    pub warnings: Vec<String>,
63    pub memory_count: usize,
64}
65
66/// 验证 pack 文件
67pub fn validate_pack(pack: &HivePack) -> ValidationResult {
68    let mut errors = vec![];
69    let mut warnings = vec![];
70
71    // pack name validation
72    if pack.pack.name.is_empty() {
73        errors.push("pack.name 缺失".into());
74    } else if !is_kebab_case(&pack.pack.name) {
75        warnings.push("pack.name 建议使用 kebab-case 格式".into());
76    }
77
78    // pack version
79    if pack.pack.version.is_empty() {
80        errors.push("pack.version 缺失".into());
81    } else if !is_semver(&pack.pack.version) {
82        warnings.push(format!("pack.version '{}' 不是标准 semver 格式", pack.pack.version));
83    }
84
85    // memories validation
86    if pack.memories.is_empty() {
87        warnings.push("memories 为空".into());
88    }
89    if pack.memories.len() > 200 {
90        errors.push(format!("memories 超过 200 条上限 (当前: {})", pack.memories.len()));
91    }
92
93    for (i, m) in pack.memories.iter().enumerate() {
94        if m.content.trim().is_empty() {
95            errors.push(format!("memories[{}].content 不能为空", i));
96        }
97
98        if MemLevel::from_str(&m.level).is_none() {
99            errors.push(format!("memories[{}].level 无效: {}", i, m.level));
100        }
101
102        if let Some(ref zone) = m.zone {
103            if MemoryZone::from_str(zone).is_none() {
104                errors.push(format!("memories[{}].zone 无效: {}", i, zone));
105            }
106        }
107
108        if let Some(ref trust) = m.trust {
109            if let Some(conf) = trust.get("confidence") {
110                if let Some(c) = conf.as_str() {
111                    if crate::trust::Confidence::from_str(c).is_none() {
112                        errors.push(format!("memories[{}].trust.confidence 无效: {}", i, c));
113                    }
114                }
115            }
116        }
117    }
118
119    ValidationResult {
120        valid: errors.is_empty(),
121        errors,
122        warnings,
123        memory_count: pack.memories.len(),
124    }
125}
126
127/// 冲突检测:检查一条 incoming memory 与 existing 列表的冲突
128#[derive(Debug)]
129pub struct ConflictResult {
130    pub status: String,
131    pub canonical_match: bool,
132    pub best_similarity: f64,
133    pub category: ConflictCategory,
134}
135
136pub fn detect_conflict(incoming: &PackMem, existing: &[&Mem]) -> ConflictResult {
137    let inc_hash = canonical_hash(&incoming.content);
138
139    // 1. 精确 canonicalHash 匹配
140    for m in existing {
141        if let Some(ref meta) = m.metadata {
142            if let Some(ex_hash) = meta.get("canonicalHash").and_then(|v| v.as_str()) {
143                if ex_hash == inc_hash {
144                    return ConflictResult {
145                        status: "exact-duplicate".into(),
146                        canonical_match: true,
147                        best_similarity: 1.0,
148                        category: ConflictCategory::Exact,
149                    };
150                }
151            }
152        }
153    }
154
155    // 2. LCS 相似度最高匹配
156    let mut best_sim: f64 = 0.0;
157    for m in existing {
158        let sim = lcs_similarity(&incoming.content, &m.content);
159        best_sim = best_sim.max(sim);
160    }
161
162    let category = ConflictCategory::from_similarity(best_sim);
163    let status = match category {
164        ConflictCategory::Exact => "exact",
165        ConflictCategory::Similar => "similar",
166        ConflictCategory::Related => "related",
167        ConflictCategory::Unrelated => "unrelated",
168    };
169
170    ConflictResult {
171        status: status.into(),
172        canonical_match: false,
173        best_similarity: best_sim,
174        category,
175    }
176}
177
178fn is_kebab_case(s: &str) -> bool {
179    if s.is_empty() { return false; }
180    s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
181        && !s.starts_with('-')
182        && !s.ends_with('-')
183}
184
185fn is_semver(s: &str) -> bool {
186    let parts: Vec<&str> = s.split('.').collect();
187    if parts.len() != 3 { return false; }
188    parts.iter().all(|p| p.parse::<u32>().is_ok())
189}
190
191/// 从 HivePack 创建导入用的记忆 (用于导入后填充运行时字段)
192pub fn create_mem_from_pack(pm: &PackMem, id: &str, now: &str, source_pack: &str) -> Mem {
193    let trust = pm.trust.as_ref().map(|t| {
194        serde_json::from_value(t.clone()).unwrap_or_else(|_| {
195            crate::trust::TrustData::new(
196                crate::trust::Confidence::Uncertain,
197                "pack-import",
198            )
199        })
200    });
201
202    let mut metadata = pm.metadata.clone().unwrap_or(serde_json::Value::Object(Default::default()));
203    if let Some(obj) = metadata.as_object_mut() {
204        obj.insert("sourcePack".into(), serde_json::Value::String(source_pack.into()));
205        obj.insert("importedAt".into(), serde_json::Value::String(now.into()));
206        if !obj.contains_key("canonicalHash") {
207            obj.insert("canonicalHash".into(), serde_json::Value::String(canonical_hash(&pm.content)));
208        }
209    }
210
211    Mem {
212        id: id.into(),
213        level: MemLevel::from_str(&pm.level).unwrap_or(MemLevel::Fact),
214        zone: pm.zone.as_ref()
215            .and_then(|z| MemoryZone::from_str(z))
216            .unwrap_or_default(),
217        content: pm.content.clone(),
218        weight: pm.weight,
219        tags: pm.tags.clone(),
220        source: Some("pack-import".into()),
221        metadata: Some(metadata),
222        last_matched: None,
223        match_count: 0,
224        loaded_count: 0,
225        referenced_count: 0,
226        last_loaded: None,
227        last_referenced: None,
228        supersedes: None,
229        superseded_at: None,
230        superseded_by: None,
231        trust,
232        created_at: now.into(),
233        updated_at: now.into(),
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_validate_valid_pack() {
243        let pack = HivePack {
244            pack: PackMeta {
245                name: "test-pack".into(),
246                version: "1.0.0".into(),
247                author: None,
248                description: None,
249                dhive_version: None,
250                tags: None,
251                memory_count: None,
252            },
253            memories: vec![
254                PackMem {
255                    content: "测试记忆".into(),
256                    level: "fact".into(),
257                    zone: None,
258                    weight: 0.5,
259                    tags: vec![],
260                    trust: None,
261                    metadata: None,
262                }
263            ],
264        };
265        let result = validate_pack(&pack);
266        assert!(result.valid);
267        assert_eq!(result.memory_count, 1);
268    }
269
270    #[test]
271    fn test_validate_empty_name() {
272        let pack = HivePack {
273            pack: PackMeta {
274                name: "".into(),
275                version: "1.0.0".into(),
276                author: None, description: None,
277                dhive_version: None, tags: None, memory_count: None,
278            },
279            memories: vec![],
280        };
281        let result = validate_pack(&pack);
282        assert!(!result.valid);
283        assert!(result.errors.iter().any(|e| e.contains("name")));
284    }
285
286    #[test]
287    fn test_validate_empty_content() {
288        let pack = HivePack {
289            pack: PackMeta {
290                name: "test".into(), version: "1.0.0".into(),
291                author: None, description: None,
292                dhive_version: None, tags: None, memory_count: None,
293            },
294            memories: vec![
295                PackMem {
296                    content: "".into(),
297                    level: "fact".into(),
298                    zone: None, weight: 0.5, tags: vec![],
299                    trust: None, metadata: None,
300                }
301            ],
302        };
303        let result = validate_pack(&pack);
304        assert!(!result.valid);
305        assert!(result.errors.iter().any(|e| e.contains("content")));
306    }
307
308    #[test]
309    fn test_validate_invalid_level() {
310        let pack = HivePack {
311            pack: PackMeta {
312                name: "test".into(), version: "1.0.0".into(),
313                author: None, description: None,
314                dhive_version: None, tags: None, memory_count: None,
315            },
316            memories: vec![
317                PackMem {
318                    content: "test".into(),
319                    level: "invalid".into(),
320                    zone: None, weight: 0.5, tags: vec![],
321                    trust: None, metadata: None,
322                }
323            ],
324        };
325        let result = validate_pack(&pack);
326        assert!(!result.valid);
327    }
328
329    #[test]
330    fn test_semver() {
331        assert!(is_semver("1.0.0"));
332        assert!(is_semver("0.3.0"));
333        assert!(!is_semver("v1.0"));
334        assert!(!is_semver("1.0"));
335    }
336
337    #[test]
338    fn test_kebab_case() {
339        assert!(is_kebab_case("typescript-core"));
340        assert!(is_kebab_case("test-pack-2"));
341        assert!(!is_kebab_case("INVALID"));
342        assert!(!is_kebab_case("-bad"));
343    }
344}