use serde::{Deserialize, Serialize};
use crate::similarity::{lcs_similarity, canonical_hash, ConflictCategory};
use crate::memory::{Mem, MemLevel, MemoryZone};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackMeta {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[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>,
}
#[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>,
}
#[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,
}
pub fn validate_pack(pack: &HivePack) -> ValidationResult {
let mut errors = vec![];
let mut warnings = vec![];
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());
}
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));
}
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(),
}
}
#[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);
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,
};
}
}
}
}
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())
}
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"));
}
}