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
@@ -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