package memory import ( "sort" "time" ) // 本文件是 `dedupe_group` 的**候选构造**侧:从一簇成员里挑出可并入的行、 // 并把簇转成候选(落库形状与 merge 候选同构,因此面板/应用/回滚三条路都不用改)。 // 它从 tidy_dedupe.go 拆出来只为守 AGENTS §10 的文件行数上限(≤500 行), // 判据与注释**逐字未动**;簇形成在 tidy_dedupe.go,元信息查询在 tidy_dedupe_meta.go。 // dedupeAbsorbable 从一簇成员里挑出**可以并入存活行**的那些(逐条过 §5.2 的安全门)。 // // 数值与否定是对**存活行**比(不是两两全比):簇的语义是「这些行说的是同一件事」, // 而同一性由存活行代表;任何一条与存活行数值/否定不一致的成员,说明它讲的是 // **另一个事实**(§5.2-2/§5.2-3),因此它**不进这一簇**(留待人工),而不是让整簇作废。 // // 前缀簇还要过相似度下限(见 dedupeGroupPrefixSimilarity);tag 集簇不需要 —— // 完全相同的 tag 集本身就是强证据,而前缀簇的「同开头」弱得多。 func dedupeAbsorbable(rule string, survivor tidyDedupeMember, members []tidyDedupeMember) []tidyDedupeMember { out := make([]tidyDedupeMember, 0, len(members)) for _, it := range members { if it.item.ID == survivor.item.ID { continue } if !sameNumbers(it.item.Content, survivor.item.Content) { continue } if !sameNegations(it.item.Content, survivor.item.Content) { continue } if rule == tidyDedupeRulePrefix && lexicalSimilarity(it.item.Content, survivor.item.Content) < dedupeGroupPrefixSimilarity { continue } out = append(out, it) } // 被吸收行的顺序也必须是确定的(幂等键按 sorted(source_ids) 算,但证据里的 // dedupe_members 会原样落库,顺序抖动会让同一个簇每次产出不同的证据文本)。 sort.Slice(out, func(i, j int) bool { return out[i].item.ID < out[j].item.ID }) return out } // dedupeGroupCandidate 把一个簇转成候选。 // // 落库字段与 merge 候选同构,因此既有面板/应用/回滚三条路都不用改形状: // `source_ids` = 被吸收行(**可多条**,`applyTidyDedupeGroup` 逐条软合并), // `target_id` = 存活行,`merged_content` = 存活行正文(**绝不改正文**)。 func dedupeGroupCandidate(cl tidyDedupeCluster, now time.Time) TidyCandidate { absorbedIDs := make([]string, 0, len(cl.Absorbed)) members := make([]string, 0, len(cl.Absorbed)) for _, it := range cl.Absorbed { absorbedIDs = append(absorbedIDs, it.ID) members = append(members, it.Content) } return TidyCandidate{ ID: mustNewTidyID(), PersonaID: cl.Survivor.PersonaID, Scope: cl.Survivor.Scope, ProjectID: cl.Survivor.ProjectID, Kind: cl.Survivor.Kind, Action: TidyDedupeGroup, SourceIDs: absorbedIDs, TargetID: cl.Survivor.ID, ProposedContent: cl.Survivor.Content, MergedContent: cl.Survivor.Content, Reason: tidyReasonDedupeGroup, Evidence: TidyEvidence{ Containment: "none", ContentA: cl.Survivor.Content, DedupeRule: cl.Rule, DedupeKey: cl.Key, ClusterSize: len(cl.Absorbed) + 1, KeptReason: cl.KeptReason, DedupeMembers: members, KindA: string(cl.Survivor.Kind), }, RuleVersion: TidyRuleVersion, Status: tidyCandidatePending, CreatedAt: now, } }