// 注入点:ConflictJudge。宿主实现冲突判定;缺省是 SimilarityJudge。
// 缺省实现 SimilarityJudge:嵌入余弦 + 关键词 Jaccard 的确定性规则判定。
// 宿主可注入自己的 judge(Replace / Coexist / Ignore,判定依据写入 metadata)。
///|
/// 冲突裁决:Replace 触发 supersede;Coexist 并存并标记;Ignore 无裁决。
pub(all) enum ConflictDecision {
Replace
Coexist
Ignore
} derive(Eq)
///|
/// 手写 Show(原 derive(Show) 输出格式保持不变:构造器名)。
pub impl Show for ConflictDecision with fn output(self, logger) {
match self {
Replace => logger.write_string("Replace")
Coexist => logger.write_string("Coexist")
Ignore => logger.write_string("Ignore")
}
}
///|
pub extend ConflictDecision with Show::{to_string, output}
///|
pub extend ConflictDecision with Eq::{not_equal, equal}
///|
/// 单个候选条目的裁决。
pub(all) struct ConflictVerdict {
existing : MemoryEntry
decision : ConflictDecision
}
///|
/// 冲突判定抽象。仅对判定为 Replace / Coexist 的候选返回裁决。
pub(open) trait ConflictJudge {
fn judge(
self : Self,
new_fact : ExtractedFact,
candidates : Array[MemoryEntry],
) -> Result[Array[ConflictVerdict], MoomemError]
}
///|
/// 缺省判定器:对每个 candidate 计算
/// sim = max(cosine(new_fact 嵌入, candidate 嵌入), 关键词 Jaccard)
/// sim >= threshold → Replace
/// [threshold - coexist_band, threshold) → Coexist
/// 否则 → Ignore(不产生裁决)
pub struct SimilarityJudge {
threshold : Double
embedder : &Embedder
coexist_band : Double
}
///|
pub fn SimilarityJudge::new(
threshold : Double,
embedder : &Embedder,
coexist_band~ : Double = COEXIST_BAND,
) -> SimilarityJudge {
SimilarityJudge::{ threshold, embedder, coexist_band, }
}
///|
pub impl ConflictJudge for SimilarityJudge with fn judge(
self,
new_fact,
candidates,
) {
let verdicts : Array[ConflictVerdict] = []
let new_vec = match self.embedder.embed([new_fact.content]) {
Ok(vecs) => if vecs.length() > 0 { vecs[0] } else { [] }
Err(_) => []
}
for cand in candidates {
let cos = cosine_similarity(new_vec, cand.embedding)
let jac = keyword_jaccard(new_fact.content, cand.keywords)
let sim = if cos > jac { cos } else { jac }
if sim >= self.threshold {
verdicts.push(ConflictVerdict::{ existing: cand, decision: Replace, })
} else if sim >= self.threshold - self.coexist_band {
verdicts.push(ConflictVerdict::{ existing: cand, decision: Coexist, })
}
}
Ok(verdicts)
}
///|
pub extend SimilarityJudge with ConflictJudge::{judge}
///|
/// 关键词 Jaccard 重叠度:|token(new) ∩ keywords(cand)| / |并集|。
/// 两侧均空时返回 0.0。
fn keyword_jaccard(
new_content : String,
cand_keywords : Array[String],
) -> Double {
let new_tokens = tokenize(new_content)
let set_a : Map[String, Bool] = Map([])
for t in new_tokens {
set_a.set(t, true)
}
let set_b : Map[String, Bool] = Map([])
for t in cand_keywords {
set_b.set(t, true)
}
if set_a.length() == 0 || set_b.length() == 0 {
return 0.0
}
let mut intersection = 0
for t in set_a.keys() {
if set_b.contains(t) {
intersection = intersection + 1
}
}
let union = set_a.length() + set_b.length() - intersection
if union == 0 {
return 0.0
}
intersection.to_double() / union.to_double()
}