fix(memory): N2 判据改为白名单形态匹配(结构上不再吞实体)
## 上一版为什么没修完(review round2 举证)
上一版把 N2 实现成**黑名单剥离**:`strings.ReplaceAll` 扫整句、把「情绪词 + 时间词 +
主语 + 程度 + 虚词 + 标点」统统删掉,**残差为空才 defer**。它的要害是剥离表里混进了
**本身就能当实体的词** —— 人称词(`我`/`我们`/`他`/`咱`/`自己`)与可作内容的单字
(`好`/`会`/`要`/`想`/`对`/`在`/`有`/`是`/`时`/`得`/`和`)。宾语恰好由这些词构成时,
实体被吞、残差为空 ⇒ **仍判 defer**:
主人不开心时会想我 → residual="" → defer ✗(持久习惯进临时层,7 天后物理删除)
主人难过时会想我们 → residual="" → defer ✗
主人心情不好时会想他 → residual="" → defer ✗
主人不开心时会想和好 → residual="" → defer ✗(`和`+`好` 两个词项各吃掉一半)
主人难过时会想妈妈 → residual="妈妈" → create ← 对照:宾语没被表吃掉
⇒ 这证伪了上一版代码里声称的不变量(「还剩一个实体就必须放行」),也说明**黑名单这
条路走不到头**:「实体」是开放集合(想我/想我们/想他/想和好/想妈妈/听音乐/咬指甲/
养猫/…),永远列不全。
## 新判据:白名单**形态**匹配
「纯情绪句」的**形态是封闭的**,据此改成「整句恰好落在形态里才 defer,其余一律放行」:
[主语]? [时间]? [程度]* 情绪核心 [程度|否定|情绪|语气]* 余部须只剩标点/空白
实现在 `retentionEmotionFormRest`(唯一判据点):
- 主语与时间词**只允许出现在句首**、各至多一次;
- 情绪核心**必须**消费到一个情绪词,否则 sawEmotion=false ⇒ 放行;
- 核心之后**只**允许「程度 / 否定 / 情绪 / 语气」四类修饰(`retentionEmotionSuffixTokens`);
- 最后要求**整句消费干净**(余部只剩标点/空白);
- 各段一律**最长词优先**(否则 `不` 会抢在 `不错` 前、`好` 会拆掉 `好累`)。
## 为什么它在**结构上**不可能再把带实体的句子判成 defer
1. 判 defer 的门槛是**整句被封闭表消费干净**。表里**没有承载内容的词** —— 没有动词、
名词、量词。任何一个形态外的字(`想`/`会`/`时`/`和`/`我`/`妈妈`/`听音乐`)都无法被
消费,必然留在余部 ⇒ 余部非空 ⇒ **放行**。
2. 主语/时间词**限死句首**:`我`/`他`/`我们` 出现在核心之后时**没有**任何词项能吃掉它
(后缀表里没有它们)—— 这正是上一版翻车的地方,现在从结构上排除了。
3. 失败模式从「**未知实体被吞**(危险、不可逆)」翻成「**未知情绪句没被认出**(安全:
多留,有 T1 与整理兜底)」。方向与 design/23 §18.3 的 fail-safe「宁可多留,不可
误弃」一致。
## 配套测试
- 正例清单补 5 条**对抗形态**(宾语由「能当实体的词」构成):`主人不开心时会想我` /
`主人难过时会想我们` / `主人心情不好时会想他` / `主人不开心时会想和好` /
`主人难过时会想妈妈` —— 与上一轮的 5 条(听音乐/咬指甲/养猫/喜欢吃甜的/找朋友聊天)
合计 10 条「带宾语的情绪句」正例;
- `TestExtractionRetention_EmotionWithObjectIsNotDeferred` 改为断言**余部**(未被形态
消费掉的部分)+最终结论:纯情绪 ⇒ 余部为空 ⇒ defer;带宾语 ⇒ 余部非空(
`时会想我` / `时会听音乐` / …)⇒ 放行。**「带宾语 ⇒ 余部非空」这条不变量现在是被
判据本身保证的**,不再是靠一张表的完备性。
## 修前必红 / 修后绿(原样输出)
修前(把 `judgeRetentionEmotion` 换成 commit 979d1cf 的黑名单实现,逐字):
```
--- FAIL: TestExtractionRetention_EmotionWithObjectIsNotDeferred (0.00s)
--- FAIL: …/主人难过时会想我们 extract_retention_test.go:122: judgeExtractionRetention("主人难过时会想我们") = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人心情不好时会想他 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人不开心时会想和好 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: …/主人不开心时会想我 … = "n2_emotion", wantEmotionDefer=false
--- FAIL: TestExtractionRetention_PositivesAreNeverDeferred (0.00s)
--- FAIL: …/情绪+代词宾语(想我) 正例 "主人不开心时会想我" 被拦成 "n2_emotion" —— 把真记忆丢进临时层
--- FAIL: …/情绪+代词宾语(想他) … --- FAIL: …/(想我们) … --- FAIL: …/(想和好) …
FAIL
```
修后:`TestExtractionRetention_{UpperCaseASCII,NegativesAreDeferred,
EmotionWithObjectIsNotDeferred,PositivesAreNeverDeferred}` 全 PASS(共 44 个子例)。
验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
This commit is contained in:
1 parent
5daba92d0f
commit
b1d51d8de2
2 files changed
+175
-91
No files matched your search
@@ -138,47 +138,107 @@ func judgeRetentionChatter(text string) retentionRule {
|
||||
|
||||
// judgeRetentionEmotion 施加 **N2**:**纯**情绪陈述(「今天好累」「心情不错」)。
|
||||
//
|
||||
// 🔴 判据是 design/03 §10.5 N2 的原文口径「**词项 + 无实体**」,不是「短 + 无数字」。
|
||||
// 实现 = 把**情绪词 / 时间词 / 主语 / 程度词 / 虚词 / 标点**统统剥掉之后,**余下为空**
|
||||
// 才判 defer;只要还剩一个实体,它就不是「情绪波动」而是**带情绪色彩的习惯或事实**
|
||||
// ⇒ 放行。
|
||||
// 🔴 判据是**白名单形态匹配**,不是黑名单剥离。这是本规则唯一正确的形态,理由:
|
||||
//
|
||||
// 为什么这条必须写成「无实体」而不是「够短」——这是**方向性**问题,不是精度问题:
|
||||
// 判成 defer 的那条记忆 7 天后从所有读路径消失(读侧时效闸,design/03 §9.6),
|
||||
// 随后被后台清扫**物理删除**(design/23 §19),**不可逆**。因此宁可漏判(噪声留进
|
||||
// 长期层,有 T1 与整理兜底),**绝不误弃**(design/23 §18.3 的 fail-safe)。
|
||||
// 反例(review 走真链路复现的误弃):`主人心情不好时会听音乐`、`主人焦虑时会咬指甲`
|
||||
// 在「够短 + 无数字就 defer」的旧判据下会被扔进临时层 —— 而那两条是**持久习惯**。
|
||||
// - 「实体」(宾语)的词表是**开放集合**—— 想我 / 想我们 / 想他 / 想和好 / 想妈妈
|
||||
// / 听音乐 / 咬指甲 / 养猫 / …,永远列不全;
|
||||
// - 而「纯情绪句」的**形态是封闭的**:`[主语]? [时间]? [程度]* 情绪词 [程度|否定|情绪|语气]* [标点]*`,
|
||||
// 且**没有其它成分**。
|
||||
//
|
||||
// 数字不再单独设防:它不在剥离表里,因此**必然留在残差里** ⇒ 带数值的句子一律放行
|
||||
// (等价于旧的 hasRetentionDigit 守卫,但由同一条「无实体」判据统一给出)。
|
||||
// 长度也不再单独设限:全局 40 rune 的上限(judgeExtractionRetention)已经兜住。
|
||||
// ⇒ 判据 = 「**整句恰好落在上面这个形态里**才 defer,其余一律放行」。
|
||||
//
|
||||
// **为什么它在结构上不可能把带实体的句子判成 defer**(不是"概率很低",是结构性):
|
||||
//
|
||||
// 1. 匹配要求**整句被消费干净**(`retentionEmotionFormRest` 返回空余部)。任何
|
||||
// 一个字符只要不在封闭表里,就会留在余部 ⇒ 判不出空 ⇒ **放行**。
|
||||
// 2. 封闭表里**没有承载内容的词**:没有动词、名词、量词。主语/时间词**只允许在句首**
|
||||
// 出现一次,情绪核心之后**只**允许「程度 / 否定 / 情绪 / 语气」四类修饰。
|
||||
// 因此核心之后的部分若含 `想`、`会`、`时`、`和`、`我`、`听音乐`、`妈妈` 这类
|
||||
// 承载内容的字,**必然消费不掉** ⇒ 余部非空 ⇒ 放行。
|
||||
// 3. 失败方向是**安全**的:认不出的形态一律**放行**(留进长期层,有 T1 与整理兜底),
|
||||
// 而**绝不**误弃。上一版(黑名单剥离)恰好相反 —— 它把「能当实体的词」也列进
|
||||
// 剥离表,于是 `主人不开心时会想我` 被剥成空、判成 defer,而那条是**持久习惯**,
|
||||
// 判 defer 即 7 天后从所有读路径消失、随后被后台清扫**物理删除**(不可逆)。
|
||||
// 黑名单那种形态**修不完**(review 举证 `会想和好` 被 `和`+`好` 吃掉),
|
||||
// 白名单把失败模式从「未知实体被吞」翻成「未知情绪句没认出」,方向才正确。
|
||||
//
|
||||
// 长度不单独设限:全局 40 rune 的上限(judgeExtractionRetention)已经兜住。
|
||||
func judgeRetentionEmotion(text string) retentionRule {
|
||||
if !containsAny(text, retentionEmotionWords()) {
|
||||
// 没有情绪词 ⇒ 与 N2 无关(本条只拦「情绪波动」这一类)。
|
||||
return retentionNone
|
||||
}
|
||||
if retentionResidual(text) != "" {
|
||||
// 剥掉情绪/时间/主语/程度/虚词后**还有实体**(听音乐 / 咬指甲 / 养猫 / …)
|
||||
// ⇒ 这是「在某种心情下会做的**稳定行为**」,属于长期记忆,放行。
|
||||
rest, sawEmotion := retentionEmotionFormRest(text)
|
||||
if !sawEmotion || rest != "" {
|
||||
// 没认出情绪核心(与 N2 无关),或**没被消费干净**(含形态外的成分,
|
||||
// 也就是"不只是情绪")⇒ 放行。两个分支都是安全方向。
|
||||
return retentionNone
|
||||
}
|
||||
return retentionEmotion
|
||||
}
|
||||
|
||||
// retentionResidual 剥掉剥离表里的全部词项,返回剩下的**实体片段**
|
||||
// (空串 = 整句只是一个情绪陈述,没有任何可长期留存的实体)。
|
||||
// retentionEmotionFormRest 尝试把 text 按「纯情绪形态」消费掉,返回
|
||||
// (未能消费的余部, 是否消费到情绪核心)。**只有余部为空且消费到情绪核心**才意味着
|
||||
// 「整句恰好是纯情绪陈述」。
|
||||
//
|
||||
// 它是 N2「无实体」那一半的唯一实现点,也是这条规则**方向正确**的关键:只有
|
||||
// 「整句由情绪词 + 时间词 + 主语 + 虚词构成」时才可能得到空残差 ⇒ 才可能 defer。
|
||||
func retentionResidual(text string) string {
|
||||
// 形态(顺序固定,各段均按**最长词优先**匹配):
|
||||
//
|
||||
// [主语]? [时间]? [程度]* 情绪核心 [程度|否定|情绪|语气]* 余部须只剩标点/空白
|
||||
//
|
||||
// 余部同时是**可断言的证据**:带宾语的句子在这里必定留下非空余部(测试钉住它)。
|
||||
func retentionEmotionFormRest(text string) (string, bool) {
|
||||
rest := text
|
||||
for _, tok := range retentionStripTokens() {
|
||||
if tok != "" {
|
||||
rest = strings.ReplaceAll(rest, tok, "")
|
||||
rest, _ = consumeRetentionPrefix(rest, retentionEmotionSubjects())
|
||||
rest, _ = consumeRetentionPrefix(rest, retentionEmotionTimeWords())
|
||||
rest = consumeRetentionPrefixes(rest, retentionEmotionDegreeWords())
|
||||
|
||||
core, ok := consumeRetentionPrefix(rest, retentionEmotionWords())
|
||||
if !ok {
|
||||
// 认不出情绪核心 ⇒ sawEmotion=false,调用方放行。
|
||||
return text, false
|
||||
}
|
||||
rest = consumeRetentionPrefixes(core, retentionEmotionSuffixTokens())
|
||||
if onlyRetentionPunctuation(rest) {
|
||||
rest = ""
|
||||
}
|
||||
return rest, true
|
||||
}
|
||||
|
||||
// consumeRetentionPrefix 在 text 的开头按**最长词优先**消费 tokens 里的一个词,
|
||||
// 返回余部与「是否消费到」。最长优先是必须的:`不` 与 `不错`、`好` 与 `好累` 同时
|
||||
// 命中时,短词先消费会把长词拆开,于是纯情绪句反而消费不干净、漏判。
|
||||
func consumeRetentionPrefix(text string, tokens []string) (string, bool) {
|
||||
best := ""
|
||||
for _, tok := range tokens {
|
||||
if tok == "" || !strings.HasPrefix(text, tok) {
|
||||
continue
|
||||
}
|
||||
if len(tok) > len(best) {
|
||||
best = tok
|
||||
}
|
||||
}
|
||||
return rest
|
||||
if best == "" {
|
||||
return text, false
|
||||
}
|
||||
return text[len(best):], true
|
||||
}
|
||||
|
||||
// consumeRetentionPrefixes 反复在句首消费,直到再也匹配不上(用于「可重复修饰」段)。
|
||||
func consumeRetentionPrefixes(text string, tokens []string) string {
|
||||
for {
|
||||
next, ok := consumeRetentionPrefix(text, tokens)
|
||||
if !ok {
|
||||
return text
|
||||
}
|
||||
text = next
|
||||
}
|
||||
}
|
||||
|
||||
// onlyRetentionPunctuation 报告 text 是否只由标点与空白构成(消费干净的最后一步:
|
||||
// 「今天好累!」剥到只剩一个标点,仍是「无实体」)。
|
||||
func onlyRetentionPunctuation(text string) bool {
|
||||
for _, r := range text {
|
||||
if !strings.ContainsRune(retentionPunctuationRunes, r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// judgeRetentionTimelyData 施加 **N4**:时效性查询(当前时间 / 天气)。
|
||||
@@ -293,41 +353,39 @@ func retentionEmotionWords() []string {
|
||||
"好累", "累了", "开心", "高兴", "难过", "不开心", "焦虑", "压力大",
|
||||
"心情", "疲惫", "难受", "无聊", "郁闷", "沮丧", "生气", "委屈",
|
||||
"烦死", "好烦", "烦躁", "兴奋", "紧张", "害怕", "孤单", "寂寞",
|
||||
// 「不错」是**情绪评价**(「心情不错」),因此它在剥离表里必须按**整词**
|
||||
// 出现:只靠虚词表里的「不」会把它剥成光杆「错」而留下残差(漏判)。
|
||||
// 「不错」是**情绪评价**(「心情不错」),因此它必须在**核心词表**里按整词
|
||||
// 出现:只靠否定词表里的「不」,`不错` 会被拆成 `不` + 光杆「错」,于是
|
||||
// 「心情不错」既认不出核心、也消费不干净 —— 一条纯情绪句反而漏判。
|
||||
"不错",
|
||||
// 光杆「累」同样要收:「主人今天真的特别累」只有一个 `累` 而不含 `好累` /
|
||||
// `累了`,漏掉它就会留下一字残差、把一条纯情绪句放行。收它**不会**带来误弃
|
||||
// —— 判 defer 的门槛恒是「剥完什么都不剩」,多收一个词只会让残差更准确。
|
||||
// 光杆「累」同理:「主人今天真的特别累」只有一个 `累` 而**不含** `好累` /
|
||||
// `累了`,漏掉它就会留下一个消费不掉的「累」、把一条纯情绪句放行。
|
||||
// 收它**不会**带来误弃 —— 判 defer 的门槛恒是「整句被形态消费干净」,
|
||||
// 多收一个情绪词只会让**本该 defer 的**句子认得出来。
|
||||
"累",
|
||||
}
|
||||
}
|
||||
|
||||
// retentionStripTokens 是 N2 判定「**无实体**」用的**剥离表**:把情绪词、时间词、
|
||||
// 主语/称呼、程度词、虚词与标点全部拼在一起,**长词在前**。
|
||||
// retentionEmotionSuffixTokens 是「情绪核心之后**允许**出现的修饰」的封闭集合:
|
||||
// 程度 / 否定 / 情绪 / 语气。**它不含任何承载内容的词** —— 这正是白名单判据在
|
||||
// 结构上不会吞掉实体的原因(核心之后一旦出现 `想`/`会`/`时`/`和`/`我` 这类字,
|
||||
// 就必然消费不掉、留下非空余部 ⇒ 放行)。
|
||||
//
|
||||
// 长词在前是**必须**的,不是风格:`好` 在程度词里、`好累` 在情绪词里,若先剥 `好`,
|
||||
// `好累` 就被拆成光杆 `累` 而永远剥不掉,残差非空 ⇒ 一条纯情绪句反而漏判。
|
||||
// (最长优先剥 ⇒ 每条词项都能按它自己的完整性被认出来。)
|
||||
//
|
||||
// 它是函数而不是包级变量(AGENTS §10 禁包级全局变量;同 tidyHabitMarkers 的手法)。
|
||||
func retentionStripTokens() []string {
|
||||
tokens := make([]string, 0, 192)
|
||||
tokens = append(tokens, retentionEmotionWords()...)
|
||||
tokens = append(tokens, retentionEmotionTimeWords()...)
|
||||
tokens = append(tokens, retentionEmotionSubjects()...)
|
||||
// 它是函数而不是包级变量(AGENTS §10 禁包级全局变量;同 tidyHabitMarkers 的手法);
|
||||
// 长度先按**最长词优先**排序,`不` 不会抢在 `不错` 前面被消费。
|
||||
func retentionEmotionSuffixTokens() []string {
|
||||
tokens := make([]string, 0, 64)
|
||||
tokens = append(tokens, retentionEmotionDegreeWords()...)
|
||||
tokens = append(tokens, retentionEmotionFunctionWords()...)
|
||||
tokens = append(tokens, retentionEmotionPunctuation()...)
|
||||
tokens = append(tokens, retentionEmotionNegations()...)
|
||||
tokens = append(tokens, retentionEmotionWords()...)
|
||||
tokens = append(tokens, retentionEmotionToneWords()...)
|
||||
sort.SliceStable(tokens, func(i, j int) bool {
|
||||
return len([]rune(tokens[i])) > len([]rune(tokens[j]))
|
||||
return len(tokens[i]) > len(tokens[j])
|
||||
})
|
||||
return tokens
|
||||
}
|
||||
|
||||
// retentionEmotionTimeWords 是 N2 剥离表里的**时间词**:它们能让一句情绪陈述显形
|
||||
// 为「今天怎么样」,但本身不含实体(「明天下午三点开会」那类带实体的句子不会因为
|
||||
// 剥掉时间词而变空)。
|
||||
// retentionEmotionTimeWords 是**只允许出现在句首**的时间词(「今天好累」)。
|
||||
// 它们让一句情绪陈述显形为「今天怎么样」,本身不含实体。
|
||||
func retentionEmotionTimeWords() []string {
|
||||
return []string{
|
||||
"今天", "明天", "昨天", "今晚", "昨晚", "今早", "此刻", "现在", "刚才", "刚刚",
|
||||
@@ -335,8 +393,9 @@ func retentionEmotionTimeWords() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// retentionEmotionSubjects 是 N2 剥离表里的**主语 / 称呼**。剥它们是为了让
|
||||
// 「主人今天好累」与「今天好累」走同一条判据 —— 主语是谁不影响「这句话有没有实体」。
|
||||
// retentionEmotionSubjects 是**只允许出现在句首**的主语 / 称呼。把它限死在句首是
|
||||
// 白名单判据的要害之一:`我`/`他`/`我们` 这些词**本身可以当实体**(「会想我」的
|
||||
// 宾语就是它),若允许它们在任意位置被消费,`主人不开心时会想我` 就会被吞成空。
|
||||
func retentionEmotionSubjects() []string {
|
||||
return []string{
|
||||
"主人", "咱们", "我们", "你们", "他们", "她们", "自己",
|
||||
@@ -344,8 +403,7 @@ func retentionEmotionSubjects() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// retentionEmotionDegreeWords 是 N2 剥离表里的**程度词**(它们只修饰情绪强度,
|
||||
// 不引入任何实体)。
|
||||
// retentionEmotionDegreeWords 是程度词(只修饰强度,不引入实体)。
|
||||
func retentionEmotionDegreeWords() []string {
|
||||
return []string{
|
||||
"非常", "特别", "稍微", "真的", "真是", "十分", "比较", "有点", "有些",
|
||||
@@ -353,28 +411,21 @@ func retentionEmotionDegreeWords() []string {
|
||||
}
|
||||
}
|
||||
|
||||
// retentionEmotionFunctionWords 是 N2 剥离表里的**虚词**(助词 / 介词 / 连词 /
|
||||
// 副词 / 情态词)。它们不承载实体,剥掉它们才能让「主人心情不好时会听音乐」剩下
|
||||
// 「听音乐」这个**实体**、从而被放行。
|
||||
func retentionEmotionFunctionWords() []string {
|
||||
return []string{
|
||||
"的时候", "的时候", "时候", "因为", "所以", "但是", "可是", "已经", "正在",
|
||||
"觉得", "感觉", "感到", "变得", "没有", "可以",
|
||||
"的", "地", "得", "了", "着", "过", "在", "会", "被", "是", "有", "和", "跟",
|
||||
"与", "都", "也", "还", "就", "而", "及", "把", "让", "给", "对", "向", "从",
|
||||
"到", "时", "之", "却", "又", "再", "要", "想", "不", "没", "能",
|
||||
}
|
||||
// retentionEmotionNegations 是情绪核心之后的否定词(「心情**不**好」)。它只能出现在
|
||||
// 后缀位;`不` 与 `没` 都不承载实体。
|
||||
func retentionEmotionNegations() []string {
|
||||
return []string{"没有", "不", "没"}
|
||||
}
|
||||
|
||||
// retentionEmotionPunctuation 是 N2 剥离表里的**标点与空白**:一句「今天好累」被
|
||||
// 剥到只剩一个逗号时,那仍然是「无实体」。
|
||||
func retentionEmotionPunctuation() []string {
|
||||
return []string{
|
||||
",", "。", "!", "?", "、", ";", ":", "…", "~", "~", "·", "-", "-",
|
||||
",", ".", "!", "?", ";", ":", "(", ")", "(", ")", " ", "\t",
|
||||
}
|
||||
// retentionEmotionToneWords 是句尾的语气词(「好累**啊**」「不错**了**」)。它们只出现
|
||||
// 在句尾,不引入实体。
|
||||
func retentionEmotionToneWords() []string {
|
||||
return []string{"啊", "呀", "啦", "哦", "喔", "嘛", "呢", "吧", "了", "的", "哎", "唉", "嗯", "哈"}
|
||||
}
|
||||
|
||||
// retentionPunctuationRunes 是「消费干净」判定允许剩下的标点与空白。
|
||||
const retentionPunctuationRunes = ",。!?、;:…~~·--,.!?;:()() \t\n"
|
||||
|
||||
// retentionTimelyQueries 是 N4 的时效性**查询整句**词表(不是单词,理由见
|
||||
// judgeRetentionTimelyData)。
|
||||
func retentionTimelyQueries() []string {
|
||||
|
||||
@@ -43,47 +43,80 @@ func retentionPositiveCases() []struct {
|
||||
// 长度安全阀:寒暄词出现,但句子长到装得下事实 ⇒ 放行。
|
||||
{"长句里的致谢", "谢谢你今天特地提醒我这件事,真是帮了大忙了"},
|
||||
|
||||
// 🔴 N2 的「**无实体**」那一半(review 走真链路复现的误弃,p1)。
|
||||
// 🔴 N2 的「**无实体**」那一半(review 两轮各自走真链路复现的误弃,p1)。
|
||||
// 它们是「**带宾语的情绪句**」——有情绪词、但情绪只是**状语**,真正要记的是
|
||||
// 后面那个稳定行为。旧判据(够短 + 无数字)会把它们扔进临时层:7 天后从
|
||||
// 所有读路径消失、再被后台清扫**物理删除**,不可逆。
|
||||
// 后面那个稳定行为/对象。判成 defer 即 7 天后从所有读路径消失、再被后台
|
||||
// 清扫**物理删除**(不可逆),因此它们必须**全部**放行。
|
||||
//
|
||||
// 前 5 条是第一轮举证的形态(宾语是名词/动宾);后 5 条是第二轮举证的
|
||||
// **对抗形态**——宾语本身由「能当实体的词」构成(`我`/`我们`/`他`/`和好`),
|
||||
// 正是黑名单剥离会吞掉、而白名单形态匹配结构上吞不掉的那一类。
|
||||
{"情绪+实体的习惯(听音乐)", "主人心情不好时会听音乐"},
|
||||
{"情绪+实体的习惯(咬指甲)", "主人焦虑时会咬指甲"},
|
||||
{"情绪+实体的习惯(养猫)", "主人心情低落时会养猫"},
|
||||
{"情绪+实体的偏好", "主人压力大的时候喜欢吃甜的"},
|
||||
{"情绪+实体的关系事实", "主人难过时会去找朋友聊天"},
|
||||
{"情绪+代词宾语(想我)", "主人不开心时会想我"},
|
||||
{"情绪+代词宾语(想我们)", "主人难过时会想我们"},
|
||||
{"情绪+代词宾语(想他)", "主人心情不好时会想他"},
|
||||
{"情绪+动词短语宾语(想和好)", "主人不开心时会想和好"},
|
||||
{"情绪+名词宾语(想妈妈)", "主人难过时会想妈妈"},
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractionRetention_EmotionWithObjectIsNotDeferred 是**上一条正例的机器守卫**
|
||||
// (拆出来是为了让「为什么这条不许被拦」的理由留在断言旁边)。
|
||||
// TestExtractionRetention_EmotionWithObjectIsNotDeferred 是**正例的机器守卫**,也是
|
||||
// 「带宾语 ⇒ 余部非空」这条**不变量**的钉子。
|
||||
//
|
||||
// 判据的**方向**比精度重要:N2 判 defer 的记忆 7 天后会被读侧时效闸隐藏、随后被
|
||||
// 后台清扫物理删除(design/23 §19),**不可逆**。因此 N2 的判据必须是
|
||||
// design/03 §10.5 的原文「**词项 + 无实体**」—— 剥掉情绪/时间/主语/程度/虚词之后
|
||||
// **余下为空**才 defer;只要还剩一个实体(听音乐 / 咬指甲 / 养猫)就必须放行。
|
||||
// design/03 §10.5 的「**词项 + 无实体**」,而实现走**白名单形态匹配**:
|
||||
//
|
||||
// 变异自证:把 judgeRetentionEmotion 退回「够短 + 无数字」的旧判据 ⇒ 本用例立刻红。
|
||||
// [主语]? [时间]? [程度]* 情绪核心 [程度|否定|情绪|语气]* 余部须只剩标点
|
||||
//
|
||||
// `rest` 就是**没能被这个形态消费掉的余部** —— 它同时是判定依据与**可断言的证据**:
|
||||
// 只要句子里有一个承载内容的字(`想` / `会` / `时` / `和` / `听音乐` / `妈妈` …),
|
||||
// 那个字一定留在余部里 ⇒ 余部非空 ⇒ 放行。
|
||||
//
|
||||
// 上一版(黑名单剥离)正是死在这里:它把「本身能当实体的词」(`我`/`他`/`我们`/`和`)
|
||||
// 也列进了剥离表,于是 `主人不开心时会想我` 被剥成空、判成 defer —— 而那是一条
|
||||
// **持久习惯**。黑名单修不完(实体是开放集合),白名单的失败模式是「认不出 ⇒ 放行」,
|
||||
// 方向才安全。
|
||||
func TestExtractionRetention_EmotionWithObjectIsNotDeferred(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
content string
|
||||
residual string // 剥完剩下的实体
|
||||
rest string // 未被「纯情绪形态」消费掉的余部
|
||||
wantDefer bool
|
||||
}{
|
||||
{content: "主人心情不好时会听音乐", residual: "听音乐", wantDefer: false},
|
||||
{content: "主人焦虑时会咬指甲", residual: "咬指甲", wantDefer: false},
|
||||
{content: "今天好累", residual: "", wantDefer: true},
|
||||
{content: "主人今天真的特别累", residual: "", wantDefer: true},
|
||||
{content: "心情不错!", residual: "", wantDefer: true},
|
||||
// 纯情绪形态 ⇒ 消费干净 ⇒ defer。
|
||||
{content: "今天好累", rest: "", wantDefer: true},
|
||||
{content: "心情不错!", rest: "", wantDefer: true},
|
||||
{content: "主人今天真的特别累", rest: "", wantDefer: true},
|
||||
{content: "主人很生气", rest: "", wantDefer: true},
|
||||
{content: "今天有点好累", rest: "", wantDefer: true},
|
||||
// 带宾语 ⇒ 余部非空 ⇒ 放行。前两组是 review 两轮各自举证的对抗输入。
|
||||
{content: "主人心情不好时会听音乐", rest: "时会听音乐", wantDefer: false},
|
||||
{content: "主人焦虑时会咬指甲", rest: "时会咬指甲", wantDefer: false},
|
||||
{content: "主人不开心时会想我", rest: "时会想我", wantDefer: false},
|
||||
{content: "主人难过时会想我们", rest: "时会想我们", wantDefer: false},
|
||||
{content: "主人心情不好时会想他", rest: "时会想他", wantDefer: false},
|
||||
{content: "主人不开心时会想和好", rest: "时会想和好", wantDefer: false},
|
||||
{content: "主人难过时会想妈妈", rest: "时会想妈妈", wantDefer: false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.content, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
text := normalizeRetentionText(tc.content)
|
||||
if got := retentionResidual(text); got != tc.residual {
|
||||
t.Fatalf("残差 = %q, want %q(残差就是 N2「无实体」那一半的判据)", got, tc.residual)
|
||||
rest, sawEmotion := retentionEmotionFormRest(normalizeRetentionText(tc.content))
|
||||
if !sawEmotion {
|
||||
t.Fatalf("%q 没被认出情绪核心 —— 这几条都含情绪词,应当认出", tc.content)
|
||||
}
|
||||
if rest != tc.rest {
|
||||
t.Fatalf("余部 = %q, want %q(余部就是「未被纯情绪形态消费掉的部分」)", rest, tc.rest)
|
||||
}
|
||||
if (rest == "") != tc.wantDefer {
|
||||
t.Fatalf("%q:余部为空 = %t 与 wantDefer = %t 不一致",
|
||||
tc.content, rest == "", tc.wantDefer)
|
||||
}
|
||||
if rule := judgeExtractionRetention(tc.content); (rule == retentionEmotion) != tc.wantDefer {
|
||||
t.Fatalf("judgeExtractionRetention(%q) = %q, wantEmotionDefer=%t —— 带宾语的与纯情绪的必须给出相反结论",
|
||||
|
||||
Reference in new issue
Block a user