docs(memory): N2 安全论证改成真实机制(位置锁 + 四类修饰)+ 两条 tripwire
应答 review round3 的 p2#1(要修的只有这一条)。
## 注释里的论证被证伪,改成真实机制
原句「封闭表里**没有承载内容的词**:没有动词、名词、量词」**字面不成立** ——
reviewer 逐表审过:情绪词表(既是核心也是后缀 token)含 `心情`(名词)、
`生气`/`害怕`/`紧张`(动词)、`压力大`(动宾);否定表里 `没有` 是动词;程度表里
`比较` 可作动词/名词;主语表里 `主人`/`我`/`他` 本身就是**指人实体**。
真正保安全的是**位置**:
1. **位置锁定** —— 主语 / 时间词只允许出现在**句首**、各至多一次 ⇒ `主人`/`我`/`你`/
`他`/`今天` 这些指人/指时实体**在文法里永远不可能落到宾语位置**;
(上一版黑名单正是死在这里:`会想我` 的 `我` 被当成主语剥掉)
2. **核心之后只允许「程度 / 否定 / 情绪 / 语气」四类修饰**,且它们只允许**紧跟**
情绪核心之后 —— 核心必须先被消费,那一段在语义上只能是**情绪的修饰**
(「心情不错」「不开心」),**装不下一个宾语短语**。
「上一版为什么错、黑名单为什么修不完」那一段**保留**(reviewer 确认该部分写得对)。
## 两条 tripwire(回应「后来人照字面加词、没有任何测试会红」)
- `TestExtractionRetention_PersistentFactsAreNeverDeferred`:**25 条**持久事实/习惯
语料(塔给的 6 条 + reviewer 扩展的 19 条,来自 review round3 的对抗构造)逐条断言
**绝不 defer**,钉住**语义后果**;语料是**可增长的**(新事故 → 一条回归断言)。
- `TestExtractionRetention_SuffixTableIsPinned`:把后缀表的**内容**逐 token 钉死
(60 个 token 的 golden),任何增删立刻红。
**为什么它比语料更接近「表的内容性质」**:语料只能覆盖**已经想到的**输入,而这张表
是判据的全部自由度 —— 往后缀里加一个名词,表变了而语料可能一条都不碰;golden 钉的
是「表变了」这个**事实本身**,改表必须显式改 golden ⇒ 变成一次可 review 的
意图声明,而不是一次静默放宽。
两条一起才够:golden 钉「表变了」,语料钉「变了以后语义还对不对」。
## 变异自证(两条 tripwire 分别会红)
变异 A:往后缀语气词表加一个名词「**妈妈**」(语料里没有任何一条会因此变 defer)
```
--- FAIL: TestExtractionRetention_SuffixTableIsPinned (0.00s)
后缀表 token 数 = 61, want 60 —— 表被改过。…
FAIL
```
⇒ golden 抓住了**语料抓不到**的改表。
变异 B:往后缀表加 `会`/`想`/`时`(看起来像无害虚词)
```
--- FAIL: TestExtractionRetention_PersistentFactsAreNeverDeferred (0.00s)
--- FAIL: …/主人心情不好时会不好
extract_retention_test.go:284: 持久事实 "主人心情不好时会不好" 被判成 "n2_emotion" —— 它会进临时层、7 天后被物理删除(不可逆)
--- FAIL: …/主人不开心时会好好想
extract_retention_test.go:284: 持久事实 "主人不开心时会好好想" 被判成 "n2_emotion" —— …
--- FAIL: TestExtractionRetention_SuffixTableIsPinned (…) 后缀表 token 数 = 63, want 60
FAIL
```
⇒ 语料把「会上线的那个后果」直接指了出来。两处变异均已还原。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿;
TestExtractionRetention 六个用例共 69 个子例全 PASS。
This commit is contained in:
1 parent
b1d51d8de2
commit
4365224c9b
2 files changed
+125
-4
No files matched your search
@@ -151,10 +151,22 @@ func judgeRetentionChatter(text string) retentionRule {
|
||||
//
|
||||
// 1. 匹配要求**整句被消费干净**(`retentionEmotionFormRest` 返回空余部)。任何
|
||||
// 一个字符只要不在封闭表里,就会留在余部 ⇒ 判不出空 ⇒ **放行**。
|
||||
// 2. 封闭表里**没有承载内容的词**:没有动词、名词、量词。主语/时间词**只允许在句首**
|
||||
// 出现一次,情绪核心之后**只**允许「程度 / 否定 / 情绪 / 语气」四类修饰。
|
||||
// 因此核心之后的部分若含 `想`、`会`、`时`、`和`、`我`、`听音乐`、`妈妈` 这类
|
||||
// 承载内容的字,**必然消费不掉** ⇒ 余部非空 ⇒ 放行。
|
||||
//
|
||||
// 2. **位置锁定 + 核心之后只允许四类修饰** —— 这才是真正保安全的一条。
|
||||
// (早先这里写的是「封闭表里没有承载内容的词」,**那句被 reviewer 证伪**
|
||||
// —— 表里确实有:`心情` 是名词,`生气`/`害怕`/`紧张`/`没有`/`比较` 可作动词,
|
||||
// `压力大` 是动宾,而主语表里的 `主人`/`我`/`他` 本身就是指人实体。)
|
||||
//
|
||||
// 真正的机制是**位置**而不是"表里没有实词":
|
||||
// - 主语 / 时间词**只允许出现在句首**、各至多一次 ⇒ `主人`/`我`/`你`/`他`/`今天`
|
||||
// 这些**指人 / 指时的实体在文法里永远不可能落到宾语位置**(上一版正是死在
|
||||
// 这里:`会想我` 的 `我` 被当成主语剥掉了);
|
||||
// - 核心之后只允许「程度 / 否定 / 情绪 / 语气」四类 token,而它们**只允许紧跟
|
||||
// 在情绪核心之后** —— 核心本身必须先被消费掉,因此那一段在语义上只能是
|
||||
// **情绪的修饰**(「心情**不错**」「**不**开心」「生气**得很**」),
|
||||
// 装不下一个宾语短语。核心之后一旦出现四类之外的东西(`想`/`会`/`时`/`和`/
|
||||
// `听音乐`/`妈妈`),**必然消费不掉** ⇒ 余部非空 ⇒ 放行。
|
||||
//
|
||||
// 3. 失败方向是**安全**的:认不出的形态一律**放行**(留进长期层,有 T1 与整理兜底),
|
||||
// 而**绝不**误弃。上一版(黑名单剥离)恰好相反 —— 它把「能当实体的词」也列进
|
||||
// 剥离表,于是 `主人不开心时会想我` 被剥成空、判成 defer,而那条是**持久习惯**,
|
||||
@@ -162,6 +174,18 @@ func judgeRetentionChatter(text string) retentionRule {
|
||||
// 黑名单那种形态**修不完**(review 举证 `会想和好` 被 `和`+`好` 吃掉),
|
||||
// 白名单把失败模式从「未知实体被吞」翻成「未知情绪句没认出」,方向才正确。
|
||||
//
|
||||
// 🔴 **后缀表不得加入承载内容的词**(名词 / 动词 / 量词 / 新的指人词)。上面第 2 条
|
||||
// 的整条安全性都建立在「四类之外一律消费不掉」上:往后缀表里塞一个名词,就会**静默**
|
||||
// 把「情绪 + 该名词作宾语」的持久事实判成 defer,而且**不会有任何行为测试变红**
|
||||
// (除非那条输入恰好在语料里)—— 这正是 review 点出的风险。两条 tripwire 钉住它:
|
||||
//
|
||||
// - `TestExtractionRetention_SuffixTableIsPinned`:把后缀表的**内容**逐 token 钉死,
|
||||
// **任何增删都立刻红**(改表必须显式改这份 golden ⇒ 变成一次可 review 的意图声明);
|
||||
// - `TestExtractionRetention_PersistentFactsAreNeverDeferred`:25 条持久事实/习惯
|
||||
// 语料逐条断言「绝不 defer」,钉住**语义后果**。
|
||||
//
|
||||
// 两条一起才够:golden 钉「表变了」,语料钉「变了以后语义还对不对」。
|
||||
//
|
||||
// 长度不单独设限:全局 40 rune 的上限(judgeExtractionRetention)已经兜住。
|
||||
func judgeRetentionEmotion(text string) retentionRule {
|
||||
rest, sawEmotion := retentionEmotionFormRest(text)
|
||||
|
||||
@@ -226,3 +226,100 @@ func TestJudgeIngestKeepsValuableItemAsCreate(t *testing.T) {
|
||||
t.Fatalf("正常条目 decision = %q, want %q(闸不许误伤)", v.Decision, IngestCreate)
|
||||
}
|
||||
}
|
||||
|
||||
// persistentFactCorpus 是**持久事实 / 习惯**语料:它们**逐条都不许**被判成 N2 的
|
||||
// defer。这份语料来自 review round3 的对抗构造(塔给的 6 条 + reviewer 扩展的 19 条,
|
||||
// 共 25 条),每一条都是「带情绪色彩、但真正要记的是情绪之后的那个持续行为/对象」。
|
||||
//
|
||||
// 判 defer 的后果是**不可逆**的:7 天后从所有读路径消失(读侧时效闸),随后被后台
|
||||
// 清扫物理删除(design/23 §19)。因此这份语料是**不可回退的 tripwire**。
|
||||
//
|
||||
// 它是**可增长的**:以后遇到新的持久事实被判 defer,往这里加一条即可 —— 加进去就
|
||||
// 从「一次线上事故」变成「一条回归断言」。
|
||||
func persistentFactCorpus() []string {
|
||||
return []string{
|
||||
"主人不开心时会好起来",
|
||||
"主人难过时会想不开",
|
||||
"主人不开心时会好好想",
|
||||
"主人心情不好时会不好",
|
||||
"主人难过时会想我妈妈",
|
||||
"主人开心时话就多",
|
||||
"主人难过会想我",
|
||||
"主人心情不好就没胃口",
|
||||
"主人开心就爱说话",
|
||||
"主人难过的时候想一个人待着",
|
||||
"主人焦虑时睡不着",
|
||||
"主人紧张时会结巴",
|
||||
"主人孤单时会找人聊天",
|
||||
"主人兴奋时会话多",
|
||||
"主人害怕时会躲起来",
|
||||
"主人心情差时会听歌",
|
||||
"主人不开心了会想和好",
|
||||
"主人压力大的时候会失眠",
|
||||
"主人最近很容易焦虑",
|
||||
"主人一个人时容易难过",
|
||||
"主人心情不好就会想家",
|
||||
"主人难过会想妈妈",
|
||||
"主人不开心就沉默",
|
||||
"主人心情不好时会找我",
|
||||
"主人焦虑时会咬指甲",
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractionRetention_PersistentFactsAreNeverDeferred 逐条断言上面那份语料
|
||||
// **一条都不判 defer**(规则闸返回 retentionNone ⇒ 走既有的 create 路径)。
|
||||
//
|
||||
// 它钉的是**语义后果**:只要有哪条持久事实被判进临时层,本用例立刻红。
|
||||
func TestExtractionRetention_PersistentFactsAreNeverDeferred(t *testing.T) {
|
||||
t.Parallel()
|
||||
corpus := persistentFactCorpus()
|
||||
if len(corpus) < 25 {
|
||||
t.Fatalf("语料只有 %d 条,不应少于 review 举证的 25 条(别把语料删小)", len(corpus))
|
||||
}
|
||||
for _, content := range corpus {
|
||||
content := content
|
||||
t.Run(content, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if rule := judgeExtractionRetention(content); rule != retentionNone {
|
||||
t.Fatalf("持久事实 %q 被判成 %q —— 它会进临时层、7 天后被物理删除(不可逆)",
|
||||
content, rule)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractionRetention_SuffixTableIsPinned 是**表的内容性质** tripwire:把「情绪
|
||||
// 核心之后允许出现的修饰 token」这张表的**内容**逐 token 钉死,任何增删都立刻红。
|
||||
//
|
||||
// 为什么它比「语料回归」更可靠:语料只能覆盖**已经想到的**输入,而这张表是判据的
|
||||
// 全部自由度 —— 往后缀表里加一个名词(比如把 `想` / `妈妈` 也当成"语气词"),表变了
|
||||
// 而语料可能一条都不碰。**golden 钉住「表变了」这个事实本身**,于是改表必须显式改这
|
||||
// 份 golden ⇒ 变成一次可 review 的意图声明,而不是一次静默放宽。
|
||||
//
|
||||
// review round3 的 p2#1 正是这个风险:注释里一句「表里没有承载内容的词」若被当真,
|
||||
// 后来人就会照着它往表里加词。现在那句话已被改成真实论证(位置锁定 + 四类修饰),
|
||||
// 这张 golden 则是它的**机器化形态**。
|
||||
func TestExtractionRetention_SuffixTableIsPinned(t *testing.T) {
|
||||
t.Parallel()
|
||||
golden := []string{
|
||||
"不开心", "压力大", "非常", "特别", "稍微", "真的", "真是", "十分", "比较", "有点",
|
||||
"有些", "没有", "好累", "累了", "开心", "高兴", "难过", "焦虑", "心情", "疲惫",
|
||||
"难受", "无聊", "郁闷", "沮丧", "生气", "委屈", "烦死", "好烦", "烦躁", "兴奋",
|
||||
"紧张", "害怕", "孤单", "寂寞", "不错", "很", "挺", "太", "好", "超",
|
||||
"蛮", "更", "最", "不", "没", "累", "啊", "呀", "啦", "哦",
|
||||
"喔", "嘛", "呢", "吧", "了", "的", "哎", "唉", "嗯", "哈",
|
||||
}
|
||||
got := retentionEmotionSuffixTokens()
|
||||
if len(got) != len(golden) {
|
||||
t.Fatalf("后缀表 token 数 = %d, want %d —— 表被改过。**改这张表会放宽/收紧 N2 的判据**:"+
|
||||
"往后缀里加名词/动词会让「情绪 + 该名词作宾语」的持久事实被静默判成 defer。"+
|
||||
"确认是**有意**改动后,同步更新这份 golden 与 persistentFactCorpus()。\ngot = %#v",
|
||||
len(got), len(golden), got)
|
||||
}
|
||||
for i := range golden {
|
||||
if got[i] != golden[i] {
|
||||
t.Fatalf("后缀表第 %d 个 token = %q, want %q(表的内容变了,见上一条的理由)",
|
||||
i, got[i], golden[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user