背景(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 的用例。
46 lines
2.2 KiB
Go
46 lines
2.2 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// 本文件是「时间性元数据位」轴(迁移 0027)的 schema 前滚:给**既有**的
|
||
// memory_items 补 `temporal` 列。
|
||
//
|
||
// 与 retention_schema.go 同一手法、同一原因:`createMemoryItemsTable` 是
|
||
// `CREATE TABLE IF NOT EXISTS`,只对**新库**生效 —— 既有部署的表早就建好了,
|
||
// 新列不会自己长出来。记忆存储必须自包含、能在任意 *sql.DB 上使用,因此这条
|
||
// 前滚不能只靠 `internal/db/migrations`。
|
||
//
|
||
// 🔴 与迁移的分工(两份真源,必须同形):权威机制是
|
||
// `internal/db/migrations/0027_memory_temporal`;这里是「任意 *sql.DB」的前滚。
|
||
// 启动顺序是「先 MigrateUp、后 NewStore」(cmd/cyrene/wire.go),因此生产库里
|
||
// 0027 先落地、这里走到 `duplicate column name` 是**预期路径**。
|
||
//
|
||
// 本列**不建索引**:目前没有任何按时间性位的读路径(它是写入侧的判定输入 +
|
||
// 事后巡检用的留痕列),提前建索引只是给每笔写入加一份没人用的维护成本。
|
||
// 将来整理侧真要按它筛(例如「把 one_off 的行捞出来复核」)再加索引不迟,
|
||
// 那时两条路径(迁移 + 本文件)一起加。
|
||
|
||
// ensureTemporalSchema 幂等地给 memory_items 补上时间性位列(列缺失时 ALTER,
|
||
// 已存在时忽略 SQLite 的 "duplicate column name" —— 那是预期路径,不是错误)。
|
||
//
|
||
// 默认值 = 空串 = 尚未分型:既有行因此**行为逐字不变**(resolveRetention 对
|
||
// 未分型的处理与 0027 之前完全一样,只跑 N1~N7)。本前滚刻意**不做**任何回填
|
||
// 判定:历史行没有对应的提取器输出,拿正文反推「它当时是一次性事件吗」正是
|
||
// 本片要消灭的那种猜测。
|
||
func ensureTemporalSchema(ctx context.Context, db *sql.DB) error {
|
||
_, err := db.ExecContext(ctx, `ALTER TABLE memory_items
|
||
ADD COLUMN temporal TEXT NOT NULL DEFAULT ''`)
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
if strings.Contains(err.Error(), "duplicate column name") {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("memory: roll forward temporal column: %w", err)
|
||
}
|