1use serde::{Deserialize, Serialize};
6use crate::similarity::{lcs_similarity, canonical_hash, ConflictCategory};
7use crate::memory::{Mem, MemLevel, MemoryZone};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PackMeta {
12 pub name: String,
14 pub version: String,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub author: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub description: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none", rename = "dhiveVersion")]
24 pub dhive_version: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub tags: Option<Vec<String>>,
28 #[serde(skip_serializing_if = "Option::is_none", rename = "memoryCount")]
30 pub memory_count: Option<u32>,
31}
32
33#[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#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct HivePack {
52 pub pack: PackMeta,
53 #[serde(default)]
54 pub memories: Vec<PackMem>,
55}
56
57#[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
66pub fn validate_pack(pack: &HivePack) -> ValidationResult {
68 let mut errors = vec![];
69 let mut warnings = vec![];
70
71 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 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 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#[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 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 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
191pub 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}