Skip to content

48 · RB→RVH 交接:从预装库删掉的词,没有送达存量用户

发出:RB(桌面端)2026-08-31 · 类型:handoff(对端要跟) 相关:30-rb-integrated-reseed-handoff.md(round30 载荷本体)· proper-noun-governance-plan.md §4.5「② 真因与 v29 prune」

1. 一句话

round30(2026-08-26)的 delete_words 载荷把 53 个词从共享资产 lampio_dict.db 里删掉了, 但两端的 seed 合并都是 ON CONFLICT DO UPDATE(只增改不删)⇒ 存量用户的本地 vocabulary 表里这 53 行一个没少。 RB 侧 2026-08-31 补了 prune;RVH 侧大概率同样中招,需要自查

2. RB 侧的证据(实测,非推断)

本机 dev 运行库 18,972 行 vs 资产 18,916 行,多出 56 行:

来源行数是什么
source='preinstalled'53round30 delete_words精确同一集合(两向差集皆空)
source='backfilled'3缓冲池回填(kyiv 是其一),正常数据,不该删

除这 56 行外两库逐行零差异primary_cefr_level / cefr_source / word_tags / pos_definitions 全等;「资产有而运行库没有」= 0)⇒ upsert 的列清单本身没问题, 唯一的缺口就是「删」这一侧。

这 53 个里有 8 个是露骨/垃圾词(bangbus hentai porno sexo vaginas masterbating masterbation redskin),其余是错误词形(alway amus ceas compos guid nurs oppos pleas posses practic puls purs rais revers trad trie upstair wast ye 等)。其中 bangbus出现在快速分级 C2 首屏——这是它被发现的路径。

3. RB 侧怎么修的

src-tauri/src/db/helpers.rs

  • 加 v29 prune,清单 = round30 载荷的镜像,沿 v18/v23/v27 三次先例带 NOT EXISTS(learning_entries) 自守(learning_entries.word → vocabulary(word) 是 RESTRICT,但 rusqlite 通道从不开 PRAGMA foreign_keys,裸 DELETE 会静默留下悬挂 FK 的孤儿)。
  • 版本锚 rb-18916-v28rb-18916-v28-prune53-2026-08-31。 🔒 资产一个字节没动(红线 #10 不受影响,两端仍是同一个物理文件),bump 的唯一目的 是让存量用户跑一次 prune ⇒ RVH 的 _preinstalledVocabVersion 不需要因此动 (判据是「资产 sha 变了两端锚都要动」,本次资产 sha 未变)。
  • 三条守卫(各自反向注入实证):G1 prune 清单 ⊇ 载荷的 delete_words + delete_words_gate1 · G2 prune 清单 ∩ 资产 = ∅ · SQL 必须带生词本自守。

真机验收:18,972 → 18,920,kyiv 未动、v27 那 9 个仍为 0、孤儿生词本条目 0

4. RVH 要做什么

先自查,不必照抄。 RVH 的 seed 路径(lib/shared/data/database/app_database.dart已经有一个 prune,但口径正好相反

dart
// 3. 清理:未被笔记本引用的 backfilled 词(被新预装库覆盖的可移除)
//   - 不清理 preinstalled 词:UPSERT 会原地更新它们
DELETE FROM vocabulary WHERE source='backfilled' AND word NOT IN (SELECT word FROM learning_entries)

⇒ 两端的清理口径互补且都不完整

preinstalled 残留清未引用的 backfilled
RB✅(v18/v23/v27/v29 的显式清单)❌(kyiv 这类一直留着)
RVH❌(注释明写刻意不清)✅(每轮 reseed 都清)

RVH 侧的判据一条 SQL(拿存量库跟资产对差集,别按词猜):

sql
-- 挂上共享资产后
SELECT v.word FROM vocabulary v
WHERE v.source='preinstalled'
  AND NOT EXISTS (SELECT 1 FROM assetdb.vocabulary x WHERE x.word=v.word);

命中非空 ⇒ 同一个缺陷在 RVH 也成立,按 RVH 自己的先例补一条 prune(务必带 「未被 learning_entries 引用」的自守)。命中为空 ⇒ RVH 的存量库本来就没灌过这批词, 写一份回执说明即可。

5. 两条对双端都成立的纪律

  • 🔒 凡是从资产里删词,同一轮必须配一条 prune 送达存量用户。 「资产删干净了」与「用户库删干净了」是两件事,而资产 SHA、体积闸、单测只看得见前者。 RB 侧已把这条钉成机械守卫(G1),RVH 侧建议同型。
  • ⚠️ prune 是「一次性、按版本字符串」的:seed 函数开头版本相等就 return,所以某台机器 一旦记下某版本,同一版本下的 prune 永不再跑。发布后才发现清单漏词 = 必须发一个新版本串, 改清单本身没用。(RB 2026-08-31 当场撞到过:dev app 在一次反向注入的窗口里重启, 52/53 被删、漏的那个永久留下。)