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
@@ -7,8 +7,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
)
|
||||
|
||||
// 本文件钉住**迁移 0026(`memory_retention_tier`)与 Store 自举 DDL 的同形与可回滚**,
|
||||
@@ -69,10 +67,9 @@ func TestMigration0026AddsColumnOverExistingRows(t *testing.T) {
|
||||
d := openRawMemoryDB(t)
|
||||
migrateUp(t, d)
|
||||
|
||||
// 造出「既有部署」:把 0026 撤掉(列与索引消失),再按**旧形态**写入两行。
|
||||
if err := db.MigrateDown(ctx, d, 1, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateDown(1): %v", err)
|
||||
}
|
||||
// 造出「既有部署」:回到 0026 **之前**(列与索引消失),再按**旧形态**写入两行。
|
||||
// 步数由账本现算:0027(`temporal`,切片 M45)压在上面,写死 1 会撤错那条。
|
||||
migrateDownToBefore(t, d, retentionMigrationID)
|
||||
if hasMemoryColumn(t, d, retentionTierColumn) || hasMemoryIndex(t, d, retentionTierIndex) {
|
||||
t.Fatalf("0026 回滚后 %s / %s 应已消失", retentionTierColumn, retentionTierIndex)
|
||||
}
|
||||
@@ -111,9 +108,7 @@ func TestMigration0026AddsColumnOverExistingRows(t *testing.T) {
|
||||
}
|
||||
|
||||
// 回滚:只撤结构,不改写任何一行的其它数据。
|
||||
if err := db.MigrateDown(ctx, d, 1, testLogger()); err != nil {
|
||||
t.Fatalf("MigrateDown(1, 二次): %v", err)
|
||||
}
|
||||
migrateDownToBefore(t, d, retentionMigrationID)
|
||||
if hasMemoryColumn(t, d, retentionTierColumn) || hasMemoryIndex(t, d, retentionTierIndex) {
|
||||
t.Fatalf("二次回滚后 %s / %s 应已消失", retentionTierColumn, retentionTierIndex)
|
||||
}
|
||||
@@ -181,11 +176,18 @@ func TestMigration0026ShapeMatchesStoreBootstrap(t *testing.T) {
|
||||
t.Fatalf("%s 侧的 %s 列形状不是 TEXT NOT NULL DEFAULT 空串:%v", name, retentionTierColumn, cols)
|
||||
}
|
||||
}
|
||||
// 自举 DDL 里新列写在最后一列(新建库的期望形状)——迁移路径上它后面可能还有
|
||||
// 自举追加的 project_id,因此只对自举侧断言顺序。
|
||||
// 自举 DDL 的尾序(新建库的期望形状):`… retention_tier, temporal`。
|
||||
// 迁移路径上这两列之后可能还有自举追加的 project_id,因此只对自举侧断言顺序。
|
||||
// (0027 之后 retention_tier **不再是**最后一列 —— temporal 追加在它之后。)
|
||||
bootCols := tableColumns(t, bootstrapped, "memory_items")
|
||||
if last := bootCols[len(bootCols)-1]; !strings.Contains(last, "|"+retentionTierColumn+"|") {
|
||||
t.Fatalf("自举建的 memory_items 最后一列 = %q, want %s", last, retentionTierColumn)
|
||||
if len(bootCols) < 2 {
|
||||
t.Fatalf("自举建的 memory_items 列数 = %d, want ≥ 2", len(bootCols))
|
||||
}
|
||||
tail := bootCols[len(bootCols)-2:]
|
||||
if !strings.Contains(tail[0], "|"+retentionTierColumn+"|") ||
|
||||
!strings.Contains(tail[1], "|"+temporalColumn+"|") {
|
||||
t.Fatalf("自举建的 memory_items 尾两列 = %v, want …, %s, %s",
|
||||
tail, retentionTierColumn, temporalColumn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user