Files
AskaEth 82e9213804 test(memory): M45 复看 P2 —— 补「temporal 不许被判定读」的源码级断言 + 注释纠正
P2-1(真缺口):r1 复看的变异 C 证明「在异步审查侧拿 item.Temporal 改判」当时
**整个 memory 包全绿** —— 已落库的行在异步侧被改判这条口径没有守卫,而异步侧
(applyVerdict / setIngestRetention)恰恰最容易被人随手接线。

新增 TestTemporalIsNotWiredIntoJudgement:扫生产代码(非 _test.go、非注释行)里
所有含 `.Temporal` 的行,逐行要求命中 temporalFieldSites 白名单(校验 3 条 / 产出 1 条,
**片段级**而不是文件级 —— 按文件放行会让同一文件里新写的接线路过),违规时报
「文件:行号: 表达式片段」;同时校验白名单每一条确实命中过,防止它老化成空头许可证。

变异实测(复看给的写法,插在 preinsert.go 的 applyVerdict 入口):
  --- FAIL: TestTemporalIsNotWiredIntoJudgement
      temporal 被接进了判定(裁决 D:它只许被记录,不许被判定读)…:
          preinsert.go:476: if item.Temporal == TemporalOneOff {
  删掉接线后全绿。

P2-2(文案与 D 矛盾):store_temporal.go 的归属清单原写「判定在 resolveRetention
(唯一入口,第 4 步)」—— D 之后第 4 步是 N1~N7、没有 temporal 步,那句等于给后来者
指了接线入口。改为:temporal 只被记录、不参与判定;要把 temporal 接进判定必须先拿到
模型真实标签分布,并只能通过 temporalFieldSites 白名单(有意的接线决定)进来。

另按复看提醒:temporal.go 头注里那两条统计 SQL 标明「**运维分析用,不属于读侧代码
路径**」,并写明读侧(timegate 与任何读侧 SQL)明令不许对 created_at/expires_at 用
datetime() —— 那是定宽文本比较,抄进查询层会静默判错。

回归:go build ./... + go vet ./... 全绿;memory/db 包测试全绿;memory -race 38.0s 全绿。
2026-10-26 19:29:16 +08:00

116 lines
6.5 KiB
Go
Raw Permalink 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 字的第三人称长期陈述(同时带『近期/目前』和持久词),
// 规则闸看不到『这是今天的一次性事件』这一位。这不是阈值问题,是**判定链缺信息位**。」
//
// 🔴 **当前状态:只观测,不参与判定**(r1 复审裁决 D,2026-10-27)。`resolveRetention`
// **不读**这一位,判定的结论集与「没有这一位」逐字相同。保留它的唯一目的是**积累
// 证据**:等真实提取跑出一批标签之后,再回答「模型到底怎么标」以及「接进判定是赚
// 还是亏」。
//
// 为什么撤(两次候选接线都被生产重放否掉,别再凭直觉改回去):
//
// ① **无条件 `one_off ⇒ temp`**(任务书初版口径):最近 500 条 active 里人工按
// 提取器口径标出的 23 条 `one_off`,其中 21 条是「约定 + 某一次没走成」形态 ⇒
// 7 天后**把约定本身物理删掉**(不可撤回)。
// ② **只放开 N6 的位置判据**(r1 复审给的改法):能翻的行在窗口内 13 条、**逐条都是
// 约定**(`约定/约好 + 今天没走成`),而复审点名的三条目标行 **0/3** 救得了
// (`…睡得比平时早` 被习惯词、`…以后不用再惦记` 被永久化标记、`自行车链条` 那条
// 没有时点词)⇒ 收益为 0、风险集中在长期约定。
//
// 还有一层方法论上的理由:以上两轮数字**全部**建立在「对历史改写正文**人工**标注」
// 上 —— 生产库里 `temporal` 列当时还不存在,模型从未在这批正文上产出过标签。
// 用人工标签去决定一条「模型标签驱动」的接线,证据链是断的。
//
// 将来真要接线,前置条件是**这份数据**(不是又一次重放):
//
// SELECT temporal, COUNT(*) FROM memory_items GROUP BY temporal;
// SELECT created_at, retention_tier, content FROM memory_items
// WHERE temporal = 'one_off' ORDER BY created_at DESC LIMIT 10;
//
// 🔴 上面两条是**运维分析用的 SQL,不属于读侧代码路径**,别把它们抄进查询层:
// timegate.go(以及任何读侧 SQL)明令**不许**对 `created_at` / `expires_at` 用
// `datetime()` 之类的 SQLite 时间函数 —— 读侧闸是**定宽文本比较**,一旦有人按这两条
// 的写法去写查询,时间格式一变就会静默判错(timegate_test.go 钉着那条反证)。
// 这里出现 `datetime(created_at)` 只是因为「人工一次性分析、离线跑、不需要精确到
// 纳秒」——**恰好**是读侧不许用的那个形态。
//
// 看 `one_off` 那 10 条里有多少是长期约定 —— 那才是「这一位能不能用」的判据。
//
// 闭集(与迁移 0027 的列值口径逐字一致):
//
// one_off 一次性事件(今天 / 这次 / 某一次发生的事)
// recurring 反复发生的规律(每天 / 每周 / 每次都…)
// durable 长期成立的事实 / 偏好 / 约定 / 称呼 / 身份
// unknown 模型拿不准,或没给出合法值
// '' 尚未分型:0027 之前的历史行 + 没做时间性判断的写入路径
// (`ManualExtractor` 的「记住 X」、网关/管理面手写、整理侧回填 ——
// 它们本来就由调用方显式给档位)
//
// 🔴 `unknown` 与 `”` 是两件事:`unknown` 是「问过模型了,它拿不准」;`”` 是
// 「这条路径没问过」。把两者折成一个值会让「模型到底答了什么」这个观测面当场消失,
// 而那正是保留这一位的全部意义。
type TemporalKind string
const (
// TemporalUnset 是「尚未分型」:历史行与没做时间性判断的写入路径(见文件头注)。
// **它不是业务取值**,也**不是**「unknown」的同义词。
TemporalUnset TemporalKind = ""
// TemporalOneOff 是一次性事件。它目前**不改变任何判定**:写入时只落库,
// 供事后统计「模型把哪些行标成了某一次发生的事」。
TemporalOneOff TemporalKind = "one_off"
// TemporalRecurring 是反复发生的规律(「每天散步十分钟」「每周三开会」)。
TemporalRecurring TemporalKind = "recurring"
// TemporalDurable 是长期成立的事实 / 偏好 / 约定 / 称呼 / 身份。
TemporalDurable TemporalKind = "durable"
// TemporalUnknown 是「问过模型了,它拿不准」。
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 }
// 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
}
}