r1 复审 p1-1(会毁数据):tag_set 簇的安全门只有数值与否定,**专有名词/引号取值不比**, 生产语料实测把 `用户的昵称是「叶酱」。` 并进 `主人的昵称是「喵喵七号」…` 的存活行, 而同库另有 4 行写着「喵喵七号已作废、应称叶酱」⇒ 一次 apply 丢掉当前事实。 修法(结构性,不是补丁): - 新增取值门 tidy_dedupe_values.go:被吸收行的**引号取值**(「」『』“”‘’")与 **拉丁 token**(≥2 字母,含 h3R3 这类混排)必须在存活行正文中一个不少; 方向单向(被吸收 ⊆ 存活),与软合并语义同向(存活行多出的取值不会丢)。 补齐 design/23 §5.1-3 要求的「专有名词 / 数值 / 否定词」三件套里缺的第一件。 - tag_set 簇也过相似度下限(dedupeGroupMinSimilarity 两档统一)。取值 0.10 而非更高: 语料实测 tag_set 合法改写(`用户可能需要提醒吃早饭…` 0.047、`叶酱有工作…` 0.109) 的余弦本来就在 0.05~0.4 —— 抬高它只会删掉合法去重,不会多挡住危险对。 实测(503 对):0.10→保留 98%、0.20→86%、0.30→63%、0.40→40%、0.50→20%。 - 回归用例:TestDedupeGroupRejectsSameTagDifferentValue(脱敏真实语料 + 同值对照组)、 TestDedupeGroupLatinTokenIsAlsoAValue、TestDedupeGroupNeverLosesAValue(簇级不变式)、 TestDedupeGroupTagSetAlsoNeedsSimilarityFloor。去掉取值门 ⇒ 前三条红;把 tag 相似度 归零 ⇒ 第四条红。 r1 复审 p2-1(数据安全):元信息查询失败时**该分区本轮不分簇**(旧实现降级成 nil map, `temporary` 恒 false ⇒ 绕开「临时行绝不进簇」红线);apply 侧补 `expires_at` 复核 (候选久置/生成后变临时 ⇒ 整簇 skip,tidySkipTemporaryRow)。用例: TestDedupeInPartitionSkipsWhenMetaUnavailable、TestTidyApplyDedupeGroupSkipsTemporaryRows。 r1 复审 p2-2(闸误杀被低报):补三类安全阀 ——「以后/从此/往后/今后」= 自此刻起永久成立、 「换成/变成/改成/搬进」= 状态已改变且新状态持续、N7 的进行中标记同样必须句首 12 字内; 「宿舍」进长期事实词表;顺手修 `<=` 的 off-by-one(改为 `<`)。 实测(生产库只读 mode=ro,active 3667): - dedupe:281 簇/594 行 → **255 簇/505 行(13.8%)**;tag_set 240 / prefix12 15; 取值门挡 76 对、相似度门挡 12 对;簇大小 2:155 3:48 4:26 5:8 6:7 7:4 8:2 9:2 18:1 20:2 (两个 20 行簇与 18 行簇的取值集合均为单一「喵喵七号」⇒ 大簇=同一事实的重写)。 - 闸:改前 2 (0.05%) → 改后 **75 (2.05%)**(n6 53 / n7 20 / n1 2);含锚点词的 27 条 逐条人读均为「约定明天中午见 / 下周留个午后」类到期即失效的安排。 - 达标代价:+目前/现在 当锚点 → 2.13%;再叠一次性事件动词表(曾/发来/分享…)→ 6.74% 且新增 172 条里 56 条是长期身份/里程碑(「i love you 文件」17 条、「海报就是助手本人」 10 条…)⇒ 仍到不了 8%,不许为凑区间放宽。 验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
64 lines
2.9 KiB
Go
64 lines
2.9 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件是 `dedupe_group` 的**元信息查询**:按分区取出「合并次数」与「是否临时层」
|
||
// 两列 —— 它们是存活规则的次要判据与一条数据安全护栏(临时行绝不进簇)。
|
||
// 从 tidy_dedupe.go 拆出来只为守 AGENTS §10 的文件行数上限(≤500 行),
|
||
// 注释与判据**逐字未动**。
|
||
|
||
// dedupeItemMetaSQL 取一个分区内每条记忆的**分簇所需元信息**:合并次数
|
||
// (存活规则的次要判据)+ 是否是临时层(`expires_at` 非空)。
|
||
//
|
||
// 两列都只读、只走分区条件,与 tidy 分区扫描同一组谓词(不含 archived/merged)。
|
||
const dedupeItemMetaSQL = `SELECT id, merge_count, expires_at FROM memory_items
|
||
WHERE persona_id = ? AND scope = ? AND project_id = ? AND status = ?`
|
||
|
||
// tidyDedupeMeta 是一条记忆的额外元信息(`MemoryItem` 的读侧 SELECT 不含这两列)。
|
||
type tidyDedupeMeta struct {
|
||
// mergeCount 是存活规则的次要判据(合并次数多的留)。
|
||
mergeCount int
|
||
// temporary 报告这条记忆在**临时层**(`expires_at` 非空,design/03 §9.6)。
|
||
//
|
||
// 🔴 临时层行**绝不进簇**,两个方向都不许:让一条长期事实并进一个 7 天后会被
|
||
// 后台**物理删除**(design/23 §19)的存活行,等于把那条事实一起删掉 ——
|
||
// 被吸收行会永远停在 `status='merged'` 且 `merged_into` 指向一个不存在的 id。
|
||
// 临时行本身也不值得合并:它的寿命本来就只剩几天。
|
||
temporary bool
|
||
}
|
||
|
||
// dedupeItemMeta 读出一个分区的 id → 元信息映射。
|
||
//
|
||
// 🔴 **失败方向是「这个分区这轮不分簇」**(不是「降级继续」):`temporary` 承载的是
|
||
// 「临时行绝不进簇」这条数据安全红线,而 nil map 会让 `temporary` 恒为 false ——
|
||
// 那等于把红线绕开(r1 复审 P2-1 实测的正是这个口子)。merge_count 缺了确实只影响
|
||
// 「等长时留哪条」,但两列同源同一次查询,为一条次要判据去承担一条红线风险不划算。
|
||
func (s *Store) dedupeItemMeta(ctx context.Context, p tidyPartition) (map[string]tidyDedupeMeta, error) {
|
||
rows, err := s.db.QueryContext(ctx, dedupeItemMetaSQL,
|
||
p.personaID, string(p.scope), p.projectID, string(tidyStatusActive))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("memory/tidy: dedupe item meta: %w", err)
|
||
}
|
||
defer func() { _ = rows.Close() }()
|
||
out := make(map[string]tidyDedupeMeta, 64)
|
||
for rows.Next() {
|
||
var (
|
||
id string
|
||
mergeCnt int
|
||
expiresAt string
|
||
)
|
||
if err := rows.Scan(&id, &mergeCnt, &expiresAt); err != nil {
|
||
return nil, fmt.Errorf("memory/tidy: scan dedupe item meta: %w", err)
|
||
}
|
||
out[id] = tidyDedupeMeta{mergeCount: mergeCnt, temporary: strings.TrimSpace(expiresAt) != ""}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, fmt.Errorf("memory/tidy: dedupe item meta rows: %w", err)
|
||
}
|
||
return out, nil
|
||
}
|