feat(memory): M45 提取器时间性元数据位 temporal 驱动留存判定
背景(M37 复审的原话):「真正卡住上游的是提取器的改写:一次性事件被写成 30~80 字的第三人称长期陈述……这不是阈值问题,是判定链缺信息位。」本片补的 就是那一位,不动 N1~N7 的任何阈值。 产出侧 - temporal.go:TemporalKind 闭集(one_off / recurring / durable / unknown, 空串 = 未分型),normalizeTemporal 只认四个字面值,其余一律归 unknown(绝不猜 同义词);提取提示词追加该枚举(模型只负责标注,判定在 Go 闭集 switch 里)。 - llmextractor:候选解析后归一并随 MemoryItem 带出;非法/缺失值不报错、不拖累 同批其他候选。 落库 - 迁移 0027_memory_temporal(up: ADD COLUMN temporal TEXT NOT NULL DEFAULT ''; down: DROP COLUMN,只撤结构、不改写任何行数据)+ NewStore 的自举前滚 (duplicate column 是预期路径);Store.Add 的同一笔 INSERT 写该列,判定与 留痕不分叉。 判定(仍是 resolveRetention 一处收口,judgeIngest 一字未改) - one_off ⇒ temp(+ expires_at),但先过 temporalSanityVeto 六条语义安全阀: 显式「记住」/ 在陈述约定 / 习惯词 / 硬性长期事实词 / 永久化 / 状态改变 —— 命中即不给结论、落回 N1~N7。依据是离线重放的实测:去掉「记住+约定」两条时 21 条长期约定会被判 temp(4.80%),全开后在最近 500 条窗口内翻转条数为 0。 - durable / recurring ⇒ long 且不再跑规则;unknown / 未分型 ⇒ 落回 N1~N7 (不许因为「正文看着像今天的事」就判 temp)。 - IngestRuleVersion d3.4 → d3.5(判定结论会变,留痕必须可分)。 验证 - go build ./... + go vet ./... 全绿;go test -count=1 ./internal/runtime/memory/... ./internal/db/... 全绿;memory -race 全绿。 - 新增 temporal_test.go(验收点 ①~⑤ + 两条交叉护栏 + 闭集/归一化)与 temporal_migration_test.go(0027 up/down/同形/真实启动顺序)。变异实测: 摘掉 one_off 支路 ⇒ ① 全红;关掉 temporalSanityVeto ⇒ 三条护栏用例全红。 - 迁移测试的「回滚 N 步」改为从 schema_migrations 现算(migrateDownToBefore), 新加一条迁移不再打红 0025/0026 的用例。
This commit is contained in:
1 parent
af61b8ccfe
commit
91101dedfb
15 files changed
+1130
-37
No files matched your search
@@ -19,7 +19,8 @@ import (
|
||||
// 2. 写进去的 `expires_at` 与读侧时效闸**同一套规范文本**
|
||||
// (定宽 30 字节 UTC —— 写错布局会让闸静默判错,本仓有历史事故);
|
||||
// 3. TTL 坏值(≤0)**绝不放行**:回落代码默认 7 天 + WARN;
|
||||
// 4. `IngestRuleVersion` 升到 `d3.4`(改判定规则必升版,否则新旧留痕无法区分);
|
||||
// 4. `IngestRuleVersion` 随判定变更升版(改判定规则必升版,否则新旧留痕无法区分;
|
||||
// 当前是 `d3.5`,M37 合入时是 `d3.4`);
|
||||
// 5. defer 是**软动作**:回滚 = 清空 `expires_at`,行与正文一字不动。
|
||||
|
||||
// readIngestExpiry 直读一行记忆的 `expires_at`(该列不在 Store.Get 的读侧面上:
|
||||
@@ -107,8 +108,8 @@ func TestIngestGateDeferWritesShortExpiresAt(t *testing.T) {
|
||||
if rev.Decision != IngestDefer || rev.Reason != retentionLowValueReason {
|
||||
t.Fatalf("闸留痕 = %+v, want decision=defer reason=%q", rev, retentionLowValueReason)
|
||||
}
|
||||
if rev.RuleVer != "d3.4" {
|
||||
t.Fatalf("闸留痕规则版本 = %q, want d3.4", rev.RuleVer)
|
||||
if rev.RuleVer != IngestRuleVersion {
|
||||
t.Fatalf("闸留痕规则版本 = %q, want %q(留痕版本必须与常量同源,不写死)", rev.RuleVer, IngestRuleVersion)
|
||||
}
|
||||
if s := r.Stats(); s.GateDeferred != 1 {
|
||||
t.Fatalf("闸计数 = %+v, want gate_deferred=1", s)
|
||||
@@ -268,16 +269,20 @@ func TestIngestGatePlanExpiresAtUsesSameLayoutAsTimeGate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIngestRuleVersionIsD34 钉住任务书验收点④:改判定规则必升版
|
||||
// TestIngestRuleVersionIsD35 钉住任务书验收点④:改判定规则必升版
|
||||
// (design/23 §18.4),否则新旧留痕无法区分。
|
||||
//
|
||||
// d3.4(切片 M37)把「判 defer」与「判 retention_tier=temp」合并成同一个判定,
|
||||
// 规则内容未改但判据的作用范围变了(每一行都在同一笔 INSERT 里落档位与有效期,
|
||||
// 且不再依赖同步闸是否开启),因此按规矩升版。
|
||||
func TestIngestRuleVersionIsD34(t *testing.T) {
|
||||
//
|
||||
// d3.5(切片 M45)给判定链补上「时间性元数据位」:`one_off` ⇒ temp、
|
||||
// `durable` / `recurring` ⇒ long、`unknown` ⇒ 旧规则。N1~N7 的规则与阈值一字未改,
|
||||
// 但同一段正文的结论会变(d3.4 只能靠词判),因此同样按规矩升版。
|
||||
func TestIngestRuleVersionIsD35(t *testing.T) {
|
||||
t.Parallel()
|
||||
if IngestRuleVersion != "d3.4" {
|
||||
t.Fatalf("IngestRuleVersion = %q, want d3.4(档位与 defer 合并为同一判定)", IngestRuleVersion)
|
||||
if IngestRuleVersion != "d3.5" {
|
||||
t.Fatalf("IngestRuleVersion = %q, want d3.5(时间性元数据位接入判定链)", IngestRuleVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user