按 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 全绿。
41 lines
1.9 KiB
Go
41 lines
1.9 KiB
Go
package memory
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
)
|
||
|
||
// 本文件是 `memory_items.temporal`(迁移 0027)在**写入路径**上的那一半:哨兵错误、
|
||
// 字段校验、以及落库时的列值编码。
|
||
//
|
||
// 其余三处的归属(别在这条链上另开一份实现):
|
||
// - **字段与语义**在 store.go 的 MemoryItem 上(结构体的字段不能跨文件声明),
|
||
// 闭集与归一化在 temporal.go;
|
||
// - **判定**在 retention.go 的 resolveRetention(唯一入口,第 4 步);
|
||
// - **DDL** 在 migrations/0027 + store_schema.go + temporal_schema.go。
|
||
//
|
||
// 拆出本文件只为守 AGENTS §10 的文件行数上限(store.go 已到 500 行)与「模块按职责
|
||
// 拆源文件」那条规范 —— 口径逐字未变。
|
||
|
||
// ErrInvalidTemporal 在条目的 Temporal(时间性元数据位,迁移 0027)不是闭集内的
|
||
// 值时返回。空串**不在**此列:它是「尚未分型」这一合法状态。
|
||
var ErrInvalidTemporal = errors.New("memory: invalid temporal kind")
|
||
|
||
// validateTemporal 只接受闭集内的值:时间性位是**枚举元数据**,一个自造的值
|
||
// ("sometimes")既判不出档位、又不该静默落库 —— 提取器已经把模型输出归一成
|
||
// unknown(temporal.go 的 normalizeTemporal),走到这里还是非法值只能是程序错误,
|
||
// 因此直接拒,而不是替它挑一个值。
|
||
func (it *MemoryItem) validateTemporal() error {
|
||
if !it.Temporal.valid() {
|
||
return fmt.Errorf("%w: %q", ErrInvalidTemporal, it.Temporal)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// temporalColumn 返回要写进 `temporal` 列的值(与 `ingestGatePlan.retention()` /
|
||
// `expiresAt()` 同族:把「条目 → 落库值」这一步收在类型自己身上,插入语句里不留
|
||
// 裸转换,也就不可能出现两处各转一次、口径分叉)。
|
||
func (it MemoryItem) temporalColumn() string {
|
||
return string(it.Temporal)
|
||
}
|