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 全绿。
116 lines
6.5 KiB
Go
116 lines
6.5 KiB
Go
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
|
||
}
|
||
}
|