refactor(memory): M45 r1 复审返工 —— temporal 位只放开 N6 位置判据

按 r1 复审裁定(p1-3items / fix-then-merge)重做三处:

P1-1/P1-2:删掉自造的六条语义安全阀(temporalSanityVeto)与约定词表
(retentionStatesAgreement)。理由被复审用两条生产行否证:它们既把该救的行
杀了(「…睡得比平时早」被习惯词、「…以后不用再惦记」被永久化标记),又挡不住
同义异形的约定(说定 / 约了)。现在 `one_off` 的全部作用是**放开 N6 的位置判据**:
judgeExtractionRetention 新增 temporal 入参 → judgeRetentionTransient →
judgeRetentionTimepointWide(位置内命中仍返 n6_timepoint,只有靠放开位置才命中
才返新增的 n6_timepoint_wide,供观测区分)。N1~N5、N7、护栏①②、N6 的 1~7 条
语义安全阀一字不动;durable / recurring 仍直接判 long(fail-safe 方向)。

P1-3:上界口径改为「时点词存在 ∧ 语义安全阀全过 ∧ 当前 long」——不依赖任何
标注,可逐条列出。实测(生产库只读副本,快照 2026-10-26):最近 500 条 active
能翻 13 条、**逐条都是「约定 + 今天没走成」**;全量 3694 条能翻 26 条(13 条
约定 + 13 条状态行)。旧的「句首时点词」口径是同义反复,已删。

已知代价(写进 temporal.go 头注与 TestTemporalOneOffWidenedPositionTouchesAgreements,
不藏在词表后面):位置窗口今天正是那批约定行的唯一保护,放开后只要模型标
one_off 它们就会判 temp;而复审点名的两条目标行本片救不了(被语义阀挡,与位置
无关)。因此这一位在当前语料上**没有可测收益**,是否保留是产品裁决。

P2-1:store.go 503 → 497 行,拆出 store_temporal.go(哨兵错误 + 字段校验 +
列值编码)。
P2-2:temporal.go 头注改准 —— `''` 是「0027 之前的历史行 + 没做时间性判断的
写入路径(ManualExtractor / 网关手写 / 整理回填,它们由调用方显式给档位)」。
P2-3:报告补语境(retention_tier 非空仅 26 行、temp 1 行、temporal 列当时还不
存在 ⇒ 重放结论对新写入没有预测力)。

测试:验收点 ①~⑤ 按新语义重写;新增「位置放开可打红」的变异靶子与「N7 位置
不放宽」「无时点词时这一位不做事」「两条目标行救不了」「约定行会翻(已知风险)」
五条边界断言。变异实测:unlocksTimepointPosition 改恒 false ⇒ 三条用例红。
回归:go build ./... + go vet ./... 全绿;memory/db 包测试与 memory -race 全绿。
This commit is contained in:
AskaEth committed 2026-10-26 19:15:08 +08:00
1 parent 91101dedfb
commit 3b9ae3f918
9 files changed
+412 -370

No files matched your search

+60 -28
View File
@@ -67,6 +67,14 @@ const (
// 没有数值、没有约束、没有硬性长期事实词。它是「一次性事件」的形态判据
// (M27 体检报告 §4 的 R4),因此落 `defer`(临时层,7 天后到期)。
retentionTimepoint retentionRule = "n6_timepoint"
// retentionTimepointWide 是对 **N6 的位置判据被时间性元数据位放开**后才命中的
// 那一种(切片 M45,迁移 0027):时点词确实在正文里,但**不在句首 12 字内**,
// 因此只有模型说「这是某一次发生的事」时才开火。
//
// 它与 retentionTimepoint 分开编号是刻意的:句式相同、结论相同,但**判据来源
// 不同**(词项位置 vs 模型位)。折成一个码,「这一位到底救了几条」就只能靠猜 ——
// 而那正是 M27 体检发现「闸形同虚设」时缺的那条观测线。
retentionTimepointWide retentionRule = "n6_timepoint_wide"
// retentionInProgress 对应 **N7 进行中状态**(2026-10-24 新增):正文说的是
// 「正在/暂时 做某件事」(正在测通道、暂时搁置、正在慢慢想),同样是时效轴上的
// 状态而不是长期事实。安全阀与 N6 完全一致(见 judgeRetentionInProgress)。
@@ -101,12 +109,15 @@ const (
// 的 N1~N5 与 M34 新增的 N6/N7)。命中返回该规则码(调用方据此定 temp 档),
// 否则返回 retentionNone。
//
// 它**不读任何状态、不写任何东西、零模型调用**:入参只有正文,出参只有规则码,
// 因此在写路径上是微秒级且可完全单测(design/03 §10.3 A 级的「零成本」要求)。
// 它**不读任何状态、不写任何东西、零模型调用**:入参只有正文与**时间性元数据位**
// (temporal,迁移 0027),出参只有规则码,因此在写路径上是微秒级且可完全单测
// (design/03 §10.3 A 级的「零成本」要求)。`temporal` 的作用**只有一处**:把 N6 的
// 位置判据放开(见 judgeRetentionTransient),其余分支一字不看它 —— 判定链没有
// 第二个入口,这一点由 retention_test.go 的源码级断言钉住。
//
// 🔴 全仓**唯一**调用点是 `resolveRetention`(retention.go):任何地方再跑一遍
// 就是「两套规则各判一遍」—— 那正是 M27 体检发现缺口(1082 次只判出 1 条)的成因,
// retention_test.go 有一条源码级断言钉住这个唯一性。
func judgeExtractionRetention(content string) retentionRule {
// 就是「两套规则各判一遍」—— 那正是 M27 体检发现缺口(1082 次只判出 1 条)的成因。
func judgeExtractionRetention(content string, temporal TemporalKind) retentionRule {
text := normalizeRetentionText(content)
if text == "" {
// 空 / 纯空白内容在 Add 的 normalize 就被拒了,走不到这里;真走到了也只
@@ -116,11 +127,7 @@ func judgeExtractionRetention(content string) retentionRule {
}
// 护栏①:显式「记住」是主人的直接指令,闸不许替他改判。它排在最前,因此
// 组合口径(护栏②)不会把主人的直接指令改成临时记忆。
//
// 切片 M45 起它的判据抽成 retentionExplicitRemember(**一处实现、两处调用**):
// 提取器的 `temporal=one_off` 元数据位(temporal.go 的 temporalVerdict)同样
// 不许推翻这条护栏 —— 否则「记住我明天三点开会」会被 7 天后删掉。
if retentionExplicitRemember(text) {
if containsAny(text, retentionRememberDirectives()) {
return retentionNone
}
// 护栏②(2026-10-24 改**组合口径**,M27 体检报告 §7 第 5 批-1):以前是
@@ -170,25 +177,12 @@ func judgeExtractionRetention(content string) retentionRule {
// N6/N7:**指示性时点 / 进行中状态**的窄规则(2026-10-24 新增,M27 §4 R4)。
// 它们与 N1~N5 同性质(纯词项 + 形态),但判的是「这份记忆的锚点是一个正在
// 过去/即将到来的时刻」,而不是「这句话没有信息」。判据与安全阀见各自函数。
if rule := judgeRetentionTransient(text); rule != retentionNone {
if rule := judgeRetentionTransient(text, temporal); rule != retentionNone {
return rule
}
return retentionNone
}
// retentionExplicitRemember 报告正文里有没有**显式要求记住**的措辞
// (「记住 / 记下 / 别忘了」,词表见 retentionRememberDirectives)。
//
// 它是护栏①的判据,也由「时间性元数据位」那一路复用(temporal.go 的
// temporalVerdict):**一条判据只有一份实现**,否则「主人的直接指令一票否决」
// 会在两条判定路上给出不同结论。
//
// 入参可以是原始正文,也可以已由 normalizeRetentionText 规范化过 ——
// 规范化是幂等的(折叠空白 + 去尾标点),重复施加不改变结果。
func retentionExplicitRemember(content string) bool {
return containsAny(normalizeRetentionText(content), retentionRememberDirectives())
}
// retentionHasAnchor 报告正文里有没有「这条记忆可能长期成立」的锚点词
// (习惯族 tidyHabitMarkers + 长期事实族 retentionDurableMarkers)。
//
@@ -276,11 +270,16 @@ func judgeRetentionOneOff(text string) retentionRule {
// ~0:`用户是上班族,需要上班(曾说明天要上班并感到不开心)`、`睡前会道晚安…
// 并约好「明天中午见」` 这类「时点词只出现在引语/从句里」的长期事实,锚点
// 不是那个时点,因此不许开火;而 `昨天用户发来…`、`用户近期在看…` 的锚点就是它。
// 🆕 **M45 起这一条是有条件的**:`temporal=one_off` 时位置判据不生效(时点词
// 出现在中段也算锚点,见 judgeRetentionTimepointWide)。上面 1~4、6、7 条语义
// 安全阀**不受这一位影响** —— 那是 r1 复审的裁定:这一位只接管「词在句子的哪个
// 位置」,不接管「这句话是不是一次性事件」。
//
// **N7(进行中状态)** 是同一时效轴的第二形态:「正在/暂时/眼下」说的事情有始有终
// (正在测通道、暂时搁置、正在慢慢想),安全阀与 N6 完全一致,并且**位置判据也一致**
// (r1 复审 P2-2:`用户会主动维护…并告诉助手她正在维护…` 里的「正在」只出现在从句,
// 是「会…」的习惯陈述,不许开火)。
// 是「会…」的习惯陈述,不许开火)。N7 的位置判据**不受**时间性位影响(M45 只放开
// N6):`one_off` + 「正在」出现在从句里仍然是习惯陈述,没有放开的理由。
//
// 🔴 **刻意不做的事**(M27 §4 的实测反例,别再重新发明):
// - 不做**长度阈值**筛(R3:≤15 字的条目抽 8 条 8/8 是可复用事实);
@@ -288,7 +287,7 @@ func judgeRetentionOneOff(text string) retentionRule {
// - 不做**一次性事件动词表**(曾/发来/分享…):实测会命中「助手曾经历三千万次
// 轮回」「用户曾发来『i love you』文件」「海报就是助手本人」这类**长期身份与
// 关系里程碑**(抽检误杀 ≈ 50%),方向错误。
func judgeRetentionTransient(text string) retentionRule {
func judgeRetentionTransient(text string, temporal TemporalKind) retentionRule {
// 安全阀 1~4:与 N1~N5 不同,它们在这里是**硬门**(命中即放行),因为 N6/N7
// 判的是「这条记忆的锚点是一个时刻」,而锚点词/数值/约束都在说「它不止是一个时刻」。
if containsAny(text, tidyHabitMarkers()) {
@@ -319,8 +318,18 @@ func judgeRetentionTransient(text string) retentionRule {
if containsAny(text, retentionChatterWords()) || containsAny(text, retentionEmotionWords()) {
return retentionNone
}
if judgeRetentionTimepoint(text) != retentionNone {
return retentionTimepoint
// 安全阀 8(**切片 M45**,唯一由时间性元数据位影响的一处):N6 的**位置判据**。
//
// - `temporal=one_off` ⇒ **不看位置**:时点词出现在句中(提取器改写出的第三人称
// 长句就是这样)也算锚点。这是本片补的那一位的全部作用 —— 它替代的是那个
// 不可靠的位置窗口,**不是**上面 1~7 条语义安全阀;
// - 其余(unknown / 未分型 / durable / recurring 已在 temporal.go 里先判成
// long,走不到这里)⇒ 位置判据照旧(`< retentionTimepointLeadRunes`)。
//
// 规则码分两档是为了让**观测面**能回答「这一条是词判的还是模型放开位置后判的」:
// 只有「恰好靠放开位置才命中」时返回 retentionTimepointWide。
if wide := judgeRetentionTimepointWide(text, temporal); wide != retentionNone {
return wide
}
if judgeRetentionInProgress(text) != retentionNone {
return retentionInProgress
@@ -340,6 +349,29 @@ func judgeRetentionTimepoint(text string) retentionRule {
return retentionNone
}
// judgeRetentionTimepointWide 是 N6 在**时间性位放开位置判据**时的形态判定。
//
// - 位置内命中 ⇒ 返回 retentionTimepoint(词项规则自己的结论,与放开前逐字一致);
// - 位置外命中**且**时点词确实存在 ⇒ 返回 retentionTimepointWide(模型放开位置后
// 才命中,留痕用);
// - 没有时点词 / 没有放开 ⇒ retentionNone。
//
// ⚠️ 它只延长**位置**,不新增任何词表,也不越过 judgeRetentionTransient 的 1~7 条
// 语义安全阀(那些在调用它之前就判完了)。
func judgeRetentionTimepointWide(text string, temporal TemporalKind) retentionRule {
rule := judgeRetentionTimepoint(text)
if rule != retentionNone {
return rule
}
if !temporal.unlocksTimepointPosition() {
return retentionNone
}
if retentionFrontedTimepoint(text) >= 0 {
return retentionTimepointWide
}
return retentionNone
}
// judgeRetentionInProgress 是 N7 的形态判定:**句首前 N 个字内**出现进行中标记
// (正在 / 暂时 / 眼下 / 这会儿)。位置判据与 N6 同口径 —— 它挡的是「正在」只出现在
// 从句里、而主句其实是习惯陈述的那些句子(r1 复审 P2-2 的实测反例)。