refactor(memory): M45 裁决 D —— temporal 位撤出判定,只保留观测

按 r1 复审裁决 D:这一位先只做观测,`resolveRetention` 不许读它。理由(实测):
无条件 one_off ⇒ temp 会当天删 21 条长期约定;只放开 N6 位置判据能翻的 13 条
逐条是约定、而复审点名的三条目标行 0/3 救得了。在「模型真实标签分布」这份证据
缺位时不该做 7 天后物理删的不可逆决定。

撤接线(结论集逐字回到 M45 之前):
- retention.go:resolveRetention 不再读 item.Temporal;与之对比 dev base
  (af61b8c)本文件**只有注释差异**,规则与判定入口逐字未变。
- extract_retention.go:judgeExtractionRetention 签名回退单参;删除
  judgeRetentionTimepointWide 与 n6_timepoint_wide 规则码。与 dev base 对比
  **同样只有注释差异** ⇒ 「结论集 = 不含 temporal 的那个集合」是结构性的,
  不是靠调用点记得别传。
- 连带取消初版的 durable / recurring ⇒ long(零影响必须是双向的)。

保留观测面(一律未动):迁移 0027 的列、自举前滚、提取器的 temporal 产出、
prompt 枚举、闭集与 normalizeTemporal、store_temporal.go 的校验/编码。
另外把 prompt 里「标成 one_off 会让它进临时层、到期消失」这句**撤接线后已是假话**
改成中性标注说明 —— 否则会诱导模型避开 one_off,观测数据直接被污染。

测试:
- TestTemporalBitDoesNotAffectRetention —— 4 类语料 × 5 个取值,(档位,规则码)
  与未分型逐字相同(核心正断言);
- TestTemporalOneOffDoesNotMakeAnythingTemporary / ...DoesNotRescueRuleHitRows
  —— 零影响的双向点名;
- TestTemporalIsStillRecorded —— one_off 的约定行仍落 temporal='one_off' 而档位
  long、expires 空(观测在、判定不动);
- TestTemporalDoesNotTouchExplicitTierOrRestrictedSource —— 人工入口与受限来源不变。
变异实测:把 `if item.Temporal == TemporalOneOff { return temp }` 插回
resolveRetention ⇒ 上述四条用例全红(FAIL 输出已贴给塔),恢复后全绿。

IngestRuleVersion 保持 d3.5,注释改准为「记录 temporal 信号,但不参与判定」
(留痕面变了:库里多了「模型当时标了什么」这一维,回退会让新旧留痕混在一起)。

P2 三条保留:store.go 500 行(拆 store_temporal.go)、temporal.go 头注('' 的口径
+ 两次被否的接线方案 + 接线前必须看的那两条统计 SQL)、报告语境段。

回归:go build ./... + go vet ./... 全绿;memory/db 包测试全绿;memory -race
38.2s 全绿。
This commit is contained in:
AskaEth committed 2026-10-26 19:21:24 +08:00
1 parent 3b9ae3f918
commit c7c0294a16
11 files changed
+280 -551

No files matched your search

+15 -59
View File
@@ -67,14 +67,6 @@ 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)。
@@ -109,15 +101,14 @@ const (
// 的 N1~N5 与 M34 新增的 N6/N7)。命中返回该规则码(调用方据此定 temp 档),
// 否则返回 retentionNone。
//
// 它**不读任何状态、不写任何东西、零模型调用**:入参只有正文与**时间性元数据位**
// (temporal,迁移 0027),出参只有规则码,因此在写路径上是微秒级且可完全单测
// (design/03 §10.3 A 级的「零成本」要求)。`temporal` 的作用**只有一处**:把 N6 的
// 位置判据放开(见 judgeRetentionTransient),其余分支一字不看它 —— 判定链没有
// 第二个入口,这一点由 retention_test.go 的源码级断言钉住。
//
// 它**不读任何状态、不写任何东西、零模型调用**:入参只有正文,出参只有规则码,
// 因此在写路径上是微秒级且可完全单测(design/03 §10.3 A 级的「零成本」要求)。
// 🔴 时间性元数据位(temporal,迁移 0027)**不是**它的入参:r1 复审裁决 D 之后
// 这一位只观测、不参与判定(见 retention.go 的 resolveRetention 头注与 temporal.go)。
// 🔴 全仓**唯一**调用点是 `resolveRetention`(retention.go):任何地方再跑一遍
// 就是「两套规则各判一遍」—— 那正是 M27 体检发现缺口(1082 次只判出 1 条)的成因。
func judgeExtractionRetention(content string, temporal TemporalKind) retentionRule {
// 就是「两套规则各判一遍」—— 那正是 M27 体检发现缺口(1082 次只判出 1 条)的成因,
// retention_test.go 有一条源码级断言钉住这个唯一性。
func judgeExtractionRetention(content string) retentionRule {
text := normalizeRetentionText(content)
if text == "" {
// 空 / 纯空白内容在 Add 的 normalize 就被拒了,走不到这里;真走到了也只
@@ -177,7 +168,7 @@ func judgeExtractionRetention(content string, temporal TemporalKind) retentionRu
// N6/N7:**指示性时点 / 进行中状态**的窄规则(2026-10-24 新增,M27 §4 R4)。
// 它们与 N1~N5 同性质(纯词项 + 形态),但判的是「这份记忆的锚点是一个正在
// 过去/即将到来的时刻」,而不是「这句话没有信息」。判据与安全阀见各自函数。
if rule := judgeRetentionTransient(text, temporal); rule != retentionNone {
if rule := judgeRetentionTransient(text); rule != retentionNone {
return rule
}
return retentionNone
@@ -270,16 +261,14 @@ func judgeRetentionOneOff(text string) retentionRule {
// ~0:`用户是上班族,需要上班(曾说明天要上班并感到不开心)`、`睡前会道晚安…
// 并约好「明天中午见」` 这类「时点词只出现在引语/从句里」的长期事实,锚点
// 不是那个时点,因此不许开火;而 `昨天用户发来…`、`用户近期在看…` 的锚点就是它。
// 🆕 **M45 起这一条是有条件的**:`temporal=one_off` 时位置判据不生效(时点词
// 出现在中段也算锚点,见 judgeRetentionTimepointWide)。上面 1~4、6、7 条语义
// 安全阀**不受这一位影响** —— 那是 r1 复审的裁定:这一位只接管「词在句子的哪个
// 位置」,不接管「这句话是不是一次性事件」。
// 🆕 **M45 曾把这一条接到时间性元数据位上,r1 复审裁决 D 又撤掉了**(原因见
// temporal.go 头注:位置放宽能翻的行逐条都是长期约定)。因此位置判据对**每一行**
// 无条件生效,与 0027 之前逐字相同。
//
// **N7(进行中状态)** 是同一时效轴的第二形态:「正在/暂时/眼下」说的事情有始有终
// (正在测通道、暂时搁置、正在慢慢想),安全阀与 N6 完全一致,并且**位置判据也一致**
// (r1 复审 P2-2:`用户会主动维护…并告诉助手她正在维护…` 里的「正在」只出现在从句,
// 是「会…」的习惯陈述,不许开火)。N7 的位置判据**不受**时间性位影响(M45 只放开
// N6):`one_off` + 「正在」出现在从句里仍然是习惯陈述,没有放开的理由。
// 是「会…」的习惯陈述,不许开火)。
//
// 🔴 **刻意不做的事**(M27 §4 的实测反例,别再重新发明):
// - 不做**长度阈值**筛(R3:≤15 字的条目抽 8 条 8/8 是可复用事实);
@@ -287,7 +276,7 @@ func judgeRetentionOneOff(text string) retentionRule {
// - 不做**一次性事件动词表**(曾/发来/分享…):实测会命中「助手曾经历三千万次
// 轮回」「用户曾发来『i love you』文件」「海报就是助手本人」这类**长期身份与
// 关系里程碑**(抽检误杀 ≈ 50%),方向错误。
func judgeRetentionTransient(text string, temporal TemporalKind) retentionRule {
func judgeRetentionTransient(text string) retentionRule {
// 安全阀 1~4:与 N1~N5 不同,它们在这里是**硬门**(命中即放行),因为 N6/N7
// 判的是「这条记忆的锚点是一个时刻」,而锚点词/数值/约束都在说「它不止是一个时刻」。
if containsAny(text, tidyHabitMarkers()) {
@@ -318,18 +307,8 @@ func judgeRetentionTransient(text string, temporal TemporalKind) retentionRule {
if containsAny(text, retentionChatterWords()) || containsAny(text, retentionEmotionWords()) {
return retentionNone
}
// 安全阀 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 judgeRetentionTimepoint(text) != retentionNone {
return retentionTimepoint
}
if judgeRetentionInProgress(text) != retentionNone {
return retentionInProgress
@@ -349,29 +328,6 @@ 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 的实测反例)。