test(memory): M45 复看 P2 —— 补「temporal 不许被判定读」的源码级断言 + 注释纠正
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 全绿。
This commit is contained in:
1 parent
c7c0294a16
commit
82e9213804
3 files changed
+99
-2
No files matched your search
@@ -8,12 +8,18 @@ import (
|
||||
// 本文件是 `memory_items.temporal`(迁移 0027)在**写入路径**上的那一半:哨兵错误、
|
||||
// 字段校验、以及落库时的列值编码。
|
||||
//
|
||||
// 其余三处的归属(别在这条链上另开一份实现):
|
||||
// 🔴 其余三处的归属(别在这条链上另开一份实现):
|
||||
// - **字段与语义**在 store.go 的 MemoryItem 上(结构体的字段不能跨文件声明),
|
||||
// 闭集与归一化在 temporal.go;
|
||||
// - **判定**在 retention.go 的 resolveRetention(唯一入口,第 4 步);
|
||||
// - **判定**仍是 `resolveRetention`(retention.go)里的 N1~N7 —— `temporal`
|
||||
// **只被记录,不参与判定**(r1 复审裁决 D),因此 `resolveRetention` 里
|
||||
// **没有、也不许有**「第 4 步看 temporal」这种东西;
|
||||
// - **DDL** 在 migrations/0027 + store_schema.go + temporal_schema.go。
|
||||
//
|
||||
// 要把 `temporal` 接进判定,前置条件写在 temporal.go 头注:先拿到模型真实标签分布
|
||||
// (那里给了两条只读 SQL),再改 `temporal_test.go` 的 temporalFieldSites 白名单 ——
|
||||
// 那张表是**有意的接线决定**的唯一入口,不许绕过它。
|
||||
//
|
||||
// 拆出本文件只为守 AGENTS §10 的文件行数上限(store.go 已到 500 行)与「模块按职责
|
||||
// 拆源文件」那条规范 —— 口径逐字未变。
|
||||
|
||||
|
||||
@@ -34,6 +34,13 @@ import "strings"
|
||||
// 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 的列值口径逐字一致):
|
||||
|
||||
@@ -2,6 +2,8 @@ package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -185,6 +187,88 @@ func readTemporalColumn(t *testing.T, store *Store, id string) TemporalKind {
|
||||
return TemporalKind(raw)
|
||||
}
|
||||
|
||||
// temporalFieldSites 是**允许**读取 `.Temporal` 的全部生产代码位置(文件 + 行内片段)。
|
||||
//
|
||||
// 只有四类,缺一不可、多一不许(多出来的那条就是「接线」):
|
||||
//
|
||||
// ① 写入侧的字段校验(store_temporal.go 的 validateTemporal,两行);
|
||||
// ② 写入侧的列值编码(store_temporal.go 的 temporalColumn);
|
||||
// ③ 产出侧的构造(llmextractor.go 把模型输出归一后放进候选条目)。
|
||||
//
|
||||
// 为什么用「片段」而不是「文件」做粒度:按文件放行会让**同一文件里**新写的接线悄悄
|
||||
// 溜过去(例如在 store_temporal.go 里加一句 `if item.Temporal == …`)。按片段放行则
|
||||
// 任何新表达式都必须先改这张表 —— 而改这张表就是一次自觉的接线决定。
|
||||
func temporalFieldSites() []struct{ file, snippet string } {
|
||||
return []struct{ file, snippet string }{
|
||||
{"store_temporal.go", "it.Temporal.valid()"},
|
||||
{"store_temporal.go", "ErrInvalidTemporal, it.Temporal"},
|
||||
{"store_temporal.go", "string(it.Temporal)"},
|
||||
{"llmextractor.go", "normalizeTemporal(c.Temporal)"},
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemporalIsNotWiredIntoJudgement 是**源码级断言**:`temporal` 只许被记录,
|
||||
// **不许被判定读**。它回答的是「零影响」能不能被后来人无声破坏 ——
|
||||
// retention_test.go 的同类断言只钉住 `judgeExtractionRetention` 的调用点,管不到
|
||||
// 「在异步审查侧拿 `item.Temporal` 改判」这种接线方式。
|
||||
//
|
||||
// 复现的缺口(r1 复审变异 C 实测):在 `preinsert.go` 的 `applyVerdict` 入口插一句
|
||||
// `if item.Temporal == TemporalOneOff { …setIngestRetention(temp)… }`,**整个 memory
|
||||
// 包全绿** —— 那时没有任何用例察觉。异步侧恰恰是最容易被随手接线的地方(它拿到的
|
||||
// 是**已落库**的行),因此这条断言必须存在。
|
||||
//
|
||||
// 判据:扫生产代码(非 `_test.go`、非注释行)里所有含 `.Temporal` 的行,逐行要求
|
||||
// 命中 temporalFieldSites 的某一条;否则报**文件 + 行号 + 表达式片段**(不只报个数)。
|
||||
// 同时校验 allowlist 的每一条**确实命中过**,防止它老化成一张空头许可证。
|
||||
func TestTemporalIsNotWiredIntoJudgement(t *testing.T) {
|
||||
t.Parallel()
|
||||
files, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
t.Fatalf("read package dir: %v", err)
|
||||
}
|
||||
allowed := temporalFieldSites()
|
||||
hit := make([]bool, len(allowed))
|
||||
var violations []string
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||||
continue
|
||||
}
|
||||
src, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", name, err)
|
||||
}
|
||||
for i, line := range strings.Split(string(src), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "//") || !strings.Contains(trimmed, ".Temporal") {
|
||||
continue
|
||||
}
|
||||
ok := false
|
||||
for j, site := range allowed {
|
||||
if name == site.file && strings.Contains(trimmed, site.snippet) {
|
||||
hit[j] = true
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
violations = append(violations, fmt.Sprintf("%s:%d: %s", name, i+1, trimmed))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
t.Fatalf("temporal 被接进了判定(裁决 D:它只许被记录,不许被判定读)。"+
|
||||
"若这是有意的接线,先拿到模型真实标签分布并改 temporal.go 头注、再改本断言的 allowlist:\n%s",
|
||||
strings.Join(violations, "\n"))
|
||||
}
|
||||
for j, site := range allowed {
|
||||
if !hit[j] {
|
||||
t.Fatalf("allowlist 项 %s: %q 已不存在 —— 这张表老化了,请同步更新"+
|
||||
"(否则它只是一张空头许可证)", site.file, site.snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeTemporal 钉住归一化口径:**只认四个字面值**,其余(空 / 中文 / 自造 /
|
||||
// 拼写变体)一律 unknown。
|
||||
//
|
||||
|
||||
Reference in new issue
Block a user