Files
Cyrene_v2/internal/runtime/memory/temporal.go
T
AskaEth 3b9ae3f918 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 全绿。
2026-10-26 19:15:08 +08:00

150 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package memory
import "strings"
// 本文件是**提取器产出侧的时间性元数据位**(`temporal`,迁移 0027):它回答
// 「这条记忆说的是**一次发生的事**,还是**反复 / 长期成立的事**」。
//
// 为什么需要这一位(M37 复审的结论,逐字):「真正卡住上游的是**提取器的改写**:
// 一次性事件被写成 30~80 字的第三人称长期陈述(同时带『近期/目前』和持久词),
// 规则闸看不到『这是今天的一次性事件』这一位。这不是阈值问题,是**判定链缺信息位**。」
//
// 🔴 **这一位只接管 N6 的「位置判据」**(2026-10-27 r1 复审裁定):词项规则里唯一
// 真正不可靠的是 **N6 的「时点词必须出现在句首 12 字内」**——提取器改写出的第三人称
// 长句本来就把时点埋在中段,窗口再放宽(12→24)就会连「同一条长期约定的重复陈述」
// 一起命中并删掉约定(M37 已量:2→14,多出的 11 条逐条都是约定)。因此:
//
// - 模型说 `one_off` ⇒ **N6 忽略位置窗口**(时点词出现在哪儿都算锚点);
// - **N6 的全部语义安全阀照旧**(习惯词 / 硬性长期事实词 / 具体约束 / 永久化 /
// 状态改变 / 寒暄情绪)—— 它们判的是「这句话不止是一个时刻」,与位置无关;
// - N1~N5、N7、护栏①② 一字不动。
//
// 🔴 r1 复审推翻的两条自造设计(别再写回来):
//
// ① **不要**给 `one_off` 再叠一层「约定/长期事实」词表否决 —— 它既把该救的行一起
// 杀了(「昨晚…睡得比平时早」被习惯词杀掉、「今天就终于把 bug 修好了,以后不用
// 再惦记」被永久化标记杀掉,而这两条正是这一位该救的),又挡不住同义异形的
// 约定(`说定`/`约了` 不在任何词表里)—— 词面否决在**两个方向**上都不安全;
// ② **不要**用「句首时点词且不触发任何阀门」当上界 —— 那种行 N6 本来就判 temp,
// 拿它当上界是同义反复,恰好把本片要救的「时点词不在句首」整类排除在外。
//
// ⚠️ **已知代价(2026-10-27 生产重放实测,r1 复审要求如实登记,不许美化)**:
// 位置窗口今天**正是**那批「约定 + 某一次没走成」行的唯一保护(它们的时点词都埋在
// 中段)。放开位置之后:
//
// - 最近 500 条 active 里**能翻的 13 条逐条都是这种约定**(`约定/约好 + 今天没走成`);
// - 全量 3694 条里能翻 26 条,其中 13 条是约定,另 13 条是「项目/宿舍/换装/探针」
// 这类状态行;
// - 而 r1 复审点名的两条目标行(`昨晚…睡得比平时早` 被习惯词、
// `今天就终于把 bug 修好了,以后不用再惦记` 被永久化标记)**本片救不了** ——
// 它们是被语义安全阀挡的,与位置无关。
//
// 也就是说:**在当前语料上,这一位没有可测收益,风险集中在长期约定上。** 是否保留
// 是产品裁决(M45 报告 §决策矩阵),不是代码判断;本文件只保证「放开位置的边界」是
// 确定的、可测的、可回退的。
//
// 闭集(与迁移 0027 的列值口径逐字一致):
//
// one_off 一次性事件(今天 / 这次 / 某一次发生的事)⇒ 放开 N6 的位置判据
// recurring 反复发生的规律(每天 / 每周 / 每次都…)⇒ long(不进规则)
// durable 长期成立的事实 / 偏好 / 约定 / 称呼 / 身份 ⇒ long(不进规则)
// unknown 模型拿不准,或没给出合法值 ⇒ **没有结论**,规则照旧
// '' 尚未分型 ⇒ 与 unknown 同路(规则照旧)。它出现在两类行上:
// ① 0027 之前的历史行;
// ② **没做时间性判断的写入路径** —— `ManualExtractor`(「记住 X」,
// 它把档位显式钉成 long,见 extract.go)、网关/管理面的手写记忆
// (`RetentionTier: long`)、整理侧回填。这些路径本来就由调用方
// 显式给档位,因此空串在这里不丢信息(档位判定第 1 步先命中)。
//
// 🔴 `unknown` 与 `”` 在判定上**走同一条路**(规则照旧),但在口径上是两件事:
// `unknown` 是「问过模型了,它拿不准」;`”` 是「这条路径没问过」。把两者折成一个值
// 会让「模型到底答了什么」这个观测面当场消失,而那正是本片要验证的东西。
type TemporalKind string
const (
// TemporalUnset 是「尚未分型」:历史行与没做时间性判断的写入路径(见文件头注)。
// **它不是业务取值**,判定上与 TemporalUnknown 同路,但含义不同。
TemporalUnset TemporalKind = ""
// TemporalOneOff 是一次性事件。它是本片唯一会**收紧**判定的时间性位:命中即
// 让 N6 忽略位置窗口(时点词出现在句子中段也算锚点)。语义安全阀照旧,因此它
// 仍然不是「无条件判 temp」——这一点是 r1 复审的裁定,别再改回去。
TemporalOneOff TemporalKind = "one_off"
// TemporalRecurring 是反复发生的规律(「每天散步十分钟」「每周三开会」)。
// 「反复」= 将来还会再发生,因此不许进临时层:命中即判 long,不再跑规则。
TemporalRecurring TemporalKind = "recurring"
// TemporalDurable 是长期成立的事实 / 偏好 / 约定 / 称呼 / 身份。同样判 long、
// 不再跑规则(方向仍是 fail-safe:多留一条)。
TemporalDurable TemporalKind = "durable"
// TemporalUnknown 是「问过模型了,它拿不准」。**它绝不是「一次性」的同义词**:
// 判定上它与未分型一样落到 N1~N7,不许因为「正文看着像今天的事」就判 temp ——
// 那等于把本片要消灭的猜测重新引回判定链。
TemporalUnknown TemporalKind = "unknown"
)
// valid 报告 t 是否落在闭集内(**含**未分型的空串:它是合法的列值,不是错误)。
func (t TemporalKind) valid() bool {
switch t {
case TemporalUnset, TemporalOneOff, TemporalRecurring, TemporalDurable, TemporalUnknown:
return true
default:
return false
}
}
// assigned 报告 t 是否**已由提取器分型**(即非空串)。它是「未分型」的唯一判据,
// 与 RetentionTier.assigned 同族,调用方不许用 `t == TemporalUnknown` 之类的
// 比较去替代它(那是「问过但拿不准」,不是「没问过」)。
func (t TemporalKind) assigned() bool { return t != TemporalUnset }
// unlocksTimepointPosition 报告这一位是否**放开 N6 的位置判据**。
//
// 只有 `one_off` 会:模型已经回答了「这是某一次发生的事」,那么时点词埋在句中
// (「…整理到一半,今天下午才收尾」)就不再是「它不是一个时刻锚点」的证据。
// `unknown` / 未分型 / durable / recurring 一律不放开。
func (t TemporalKind) unlocksTimepointPosition() bool {
return t == TemporalOneOff
}
// normalizeTemporal 把模型输出的自由文本归一到上面四个值。
//
// 🔴 **只做大小写折叠与首尾空白裁剪,绝不做同义词猜测**(「一次性」「once」→ 都不认):
// 认不出来的答案就是解析失败,一律落 TemporalUnknown —— 这正是任务书
// 「模型只能选那四个值,解析失败一律归 unknown(绝不猜)」的字面实现。
// 猜同义词的代价是单向的:猜成 one_off 就删记忆,猜成别的只是多留一条。
func normalizeTemporal(s string) TemporalKind {
switch TemporalKind(strings.ToLower(strings.TrimSpace(s))) {
case TemporalOneOff:
return TemporalOneOff
case TemporalRecurring:
return TemporalRecurring
case TemporalDurable:
return TemporalDurable
default:
// 空值、非法值、拼写错误、模型自造的新值:全部归 unknown(**不报错**)。
// 一条候选记忆的价值不取决于它的时间性位是否写对,因此解析失败绝不
// 让整批提取失败(那会静默丢掉同批的其他候选)。
return TemporalUnknown
}
}
// temporalTier 给出**模型直接给出的档位结论**(只由 resolveRetention 第 4 步调用)。
//
// 只有 `durable` / `recurring` 有结论(long):模型已经回答了「这不是一次性事件」,
// 此时再让词项规则判 temp 就是把「判定链缺的那一位」又丢掉;方向是 fail-safe(多留)。
//
// `one_off` **不在这里**出结论 —— 它作为**输入**进规则层(放开 N6 的位置判据),
// 语义安全阀照旧(r1 复审裁定,见文件头注);`unknown` / 未分型同样没有结论,
// 规则照旧。
func temporalTier(t TemporalKind) (RetentionTier, bool) {
switch t {
case TemporalDurable, TemporalRecurring:
return RetentionLong, true
default:
return RetentionTierUnset, false
}
}