Skip to content

25 · lemmatizer 资产 v26 修正 —— RVH 侧接收完成确认(回 RB doc 24)

物理仓库位置:~/reading_vocab_helper/docs/cross-end/25-rvh-lemmatizer-v26-confirmation.md 日期:2026-08-18 · 方向:RB 产出 → RVH 整包接收(纯消费方),红线 #5e / #10 上游交接单:~/reading-browser/docs/cross-end/24-rb-lemmatizer-asset-v26-reseed-handoff.md 关闭缺陷:backlog「P2 — lemmatizer 资产:高频功能词被映射成生僻/错误 base」(2026-08-17 立项)


§1 一句话

RVH 上报的 rather → rath(古语词以正经生词身份进复习卡)已由 RB 侧修完,178 个 surface 的归一结果改变。RVH 侧完成代码/文档对齐 + 回归,双端 byte-equal 复验通过。


§2 RVH 侧改动清单

#文件说明
1🔴 预装库版本 bumplib/shared/data/database/app_database.dart:211_preinstalledVocabVersion 25 → 26,并补 v26 版本注释日志
2预装库整包接收assets/databases/lampio_dict.dbRB 会话已覆盖,SHA 2a38e5c2…;本仓提交
3测试 fixture 同步assets/nlp/{surface_to_base,base_forms}.json⚠️ 交接单 §8 未列,但必须做 —— 见 §3
4红线 #5e 更新CLAUDE.md两个 SHA256 + 行数;删「base_forms 未变」;新增 v26 修正段(REMAP/SELF/KEEP/PARK + 两步修法警告)
5行数锚lib/core/nlp/lemmatizer.dart:18-19140,371 → 140,281 / 101,646 → 101,709(原注释比 v18 还旧)
6行数锚assets/nlp/README.md同上
7契约数字docs/cross-end/13-lemmatizer-fold-reseed-handoff.md§1 DDL 行注 + lemma_meta 表值;顶部加「已被 v26 取代」横幅;patch 脚本引用改指 build_dict.rs
8前向指针docs/database/schema.md:175v54 as-built 条目不改,追加 v26 取代说明
9回归用例 +5test/core/nlp/lemmatizer_dictionary_test.dartv26-Av26-E,见 §4
10陈旧断言修正test/core/nlp/lemmatizer_test.dart:391见 §5
11证据文件docs/cross-end/25-v26-changed-surfaces{,-rvh}.{txt,csv}178 个受影响 surface 及其 RVH 归一输出

不改CHANGELOG.md 历史记录、docs/decisions.md ADR、rvh/docs/plans/lemmatizer-dictionary-refactor-rvh.mddocs/cross-end/13 §3 验证回执 —— 均为 as-built 记录(含真机日志、当时的 db SHA), 改写等于伪造历史。一律用「前向指针 / 顶部横幅」标注被取代,不就地篡改数字。


§3 ⚠️ 交接单 §8 漏了一项:assets/nlp/*.json fixture

RB 会话只整包覆盖了 assets/databases/lampio_dict.db。但 RVH 仓里 assets/nlp/ 下那两份 JSON 仍是 v18 版本(SHA 3094c829… / ca75b179…)。它们不是 shipped runtime 资产,却是两个活跃消费方的输入:

  • bin/phase0_normalize.dart —— 红线 #5e 跨端 byte-equal 验证工具本身
  • test/core/nlp/lemmatizer_test_setup.dart —— 全部 lemmatizer 单测的 fixture

不同步的后果是验证工具与被验证物脱钩:单测会继续对着旧资产跑绿、phase0 CSV 会拿旧资产产出, 于是「双端 byte-equal」这条红线的检验手段自己失效。已从 RB 权威产物复制并 cmp 逐字节复验:

a4ef3b1047c7252556721525da97c378d57b1c4686652567b3e37ca02aabcf59  assets/nlp/base_forms.json      (101,709)
98a24cf47ac457a05f1e172211bdd14ed59291732ada3fbfc5fcaf4e52e31d97  assets/nlp/surface_to_base.json (140,281)

与交接单 §3 表格逐字符一致。另复验 db 内 lemma_* 表与 JSON 逐条相等lemma_surface_to_base 140,281 == JSON map / lemma_base_forms 101,709 == JSON base,全量 dict 比较 True), lemma_meta 四行 provenance 同步,vocabulary 18,898 行未动(符合 §6「只重烤 lemma 表」)。

建议下次交接单把 fixture 同步列进 §8 —— 它和 db 是同一份数据的两个副本, 只更新其一会让红线的验证手段静默失真。


§4 回归用例(v26-Av26-E

按交接单 §2.1 的要求,自映射组layer == 'base' 一起断言

用例内容
v26-ASELF 组 12 词(rather latter his outer always sometimes inner us during yes opera clothes)自映射 且 layer == base
v26-BREMAP 组 9 条(based→base · changed/changing→change · raised/raises→raise · tries→try · latest→late · routing→route · batteries→battery)
v26-CKEEP 哨兵 5 条(people→person · could→can · its→it · better→good · longer→long)不得被误伤
v26-D修正后的词仍满足 normalize 幂等性
v26-Erath 仍在 base_forms(Layer 3 陷阱仍然存在 → 证明 A 的 layer 断言有效)

layer 断言确实有牙 —— 注入缺陷实测

模拟「只删 surface_to_base 映射、漏补 base_forms」(把 rather/always 从 base_forms 里拿掉后重载):

rather -> lemma=rath   layer=suffix-validated
always -> lemma=alway  layer=suffix-validated

Layer 3 后缀规则原样把残根推了回来,与交接单 §2.1 描述完全吻合。 layer == 'base' 是这条复发路径上的唯一durable 守卫。


§5 交接单未预料到的一处:一条陈旧断言把 bug 写成了「已知限制」

test/core/nlp/lemmatizer_test.dart 里存在:

dart
// during/outstanding 是 AGID 数据限制(动词 dure / outstand 在词典中),
// 双端等量映射。待 T-B 多义消歧 / 词频权重解决。
expect(lemmatizer.lemmatize('during'), equals('dure'));
expect(lemmatizer.lemmatize('outstanding'), equals('outstand'));

这条断言在锁定缺陷行为during / outstanding 都在 v26 的 SELF-89 名单里, 资产修好后它是唯一一条变红的既存用例。已改为断言正确行为并更新注释指向红线 #5e。

记一笔纪律:把已知缺陷写成断言时,注释里那句「待 X 解决」不会在 X 解决时提醒任何人。 这次是靠测试变红被动发现的。


§6 跨端 byte-equal 复验

6.1 ⚠️ 交接单 §8.4 指的那组证据文件已经陈旧(与 v26 无关)

交接单要求「复验 docs/cross-end/phase0_inputs.txt + rb.csv/rvh.csv 仍逐字节相同」。 实跑发现新生成的 RVH CSV 与仓库里存档的 rvh.csv 不同。排查结论:不是 v26 回归

那两个 CSV 提交于 2026-04-26(RB 0a8e735),早于 2026-04-28 的「词典优先」架构重构(Bug B 修复)。 证据是它们的 layer 取值是旧架构exception / irregular / suffix / fallback, 而现行架构是 dictionary / base / suffix-validated / fallback;内容上还留着 Bug B 的过度剥离:

存档 rvh.csv            现在(正确)
Paris,pari,suffix   →   Paris,paris,base
James,jam,suffix    →   James,james,base
women,women,fallback →  women,woman,dictionary
movies,movy,suffix  →   movies,movie,dictionary

rb.csvrvh.csv 彼此仍逐字节相同(两端一起陈旧),所以交接单那句复验按字面执行会通过, 但它证明的是「四个月前两端一致」,不是「今天两端一致」。这组文件已不能作为 v26 的验收证据。

6.2 实际做的复验:两端当场重跑再 diff

输入集词数RB(cargo run --example phase0_normalize --release) vs RVH(dart build cli
test/data/lemma-regression.txt(红线 #5e 指定)99byte-identical
docs/cross-end/phase0_inputs.txt(两端均以 v26 资产重跑)131byte-identical
本轮改动的 178 个 surface(最强检验)178byte-identical

第三行是交接单没要求、但真正对准改动面的检验:直接 diff 新旧资产得出受影响 surface 全集 (removed 89 + remapped 89 = 178,base_forms +63/−0,与 §3/§5 数字逐一吻合),两端跑完全等。

交接单「178 个改动 surface 与 phase0 输入词零交集」的说法经复核成立(交集为空集, lemma-regression.txt 同样为空)—— CSV 对不上是另一个独立原因。

遗留(RB 仓,非 RVH 可改):docs/cross-end/{rb,rvh}.csv + phase0_inputs.txt 建议重新生成✅ 已处置(2026-08-24,方案 A):两个 csv 已用两端当前实现重新生成(输入不变,131 词), cmp 逐字节相同;RB docs/cross-end/README.md 数据资产行补了「基线时点 2026-08-24 + layer 词表 dictionary/base/fallback(+empty)」作新鲜度指纹。经过与勘误详见 RB docs/cross-end/26-rvh-phase0-evidence-stale-report.md(含顶部处置回执)。 同批把本文 §6.3 记的遗留项也修了 —— RVH CLAUDE.md 红线 #5e 的验证命令段 dart compile exedart build cli,并已实跑照抄验证。

6.3 工具链变化

CLAUDE.md 红线 #5e 写的 dart compile exe bin/phase0_normalize.dart 在 Dart 3.10.4 下已失败

'dart compile' does not support build hooks, use 'dart build' instead.

objective_c 等包引入了 build hooks。)改用 dart build cli -o <dir>,产物在 <dir>/bundle/bin/phase0_normalize,stdout 干净、可直接重定向。原纪律「不要用 dart run」 的理由(build hooks 污染 stdout)依然成立,只是实现命令换了。


§7 版本 bump 的判断(与 RB 相反,已核实非照搬)

交接单 §8.0 要求 bump,理由是 RVH 与 RB 的读取路径不同。逐条在代码里核实过:

  • lemmatizer_flutter_loader.dart:24_ensureLemmaDbReady() 读 documents 目录的 preinstalled_vocab.db不是 asset 本身
  • 刷新那份拷贝的唯一动作是 app_database.dart:487writeAsBytes,而 :476 if (currentVersion >= _preinstalledVocabVersion) return; 会早退跳过
  • 自愈路径 lemmatizer_flutter_loader.dart:60-70 只跑 SELECT name FROM sqlite_master WHERE type='table' AND name='lemma_surface_to_base'needsCopy = has.isEmpty。存量安装的旧拷贝表在、数据旧 → 判定「无需重拷」

结论成立:不 bump 则本次修复对存量 RVH 用户完全无效。代价仅是启动时重跑一次 18,898 行 vocabulary UPSERT(payload 未变,纯耗时)。


§8 验收

结果
flutter analyze(改动文件)0 error(仅 info 级 lint;仓库既有 1 个 error 在 tools/vocabulary_builder/fix_empty_definitions.dart,与本轮无关)
flutter test test/core/nlp/100 passed
flutter test(全量)1181 passed / 5 skipped,0 failed
资产 SHA256 双端✅ 两个都与交接单 §3 逐字符一致
db lemma_* ↔ JSON✅ 全量逐条相等(140,281 / 101,709)
vocabulary18,898 行未动 ✅
跨端 phase0 diff✅ 三个输入集全部 byte-identical(§6.2)

§9 存量数据清理(RVH 侧代码零改动)

按交接单 §9.1b 裁定:不做本地迁移、不落 migration(存量用户全是会重装的内部内测用户)。

⚠️ 但重装清不掉 —— 这些行同时在 Supabase 的 user_* 表里,重装重登后 sync pull 会原样拉回, 且拉得进来(本轮没动 vocabularyrath 仍是预装库 headword,父行/FK 存在性检查照样放行)。

清理在云端做一次即可、两端共用,SQL 由 RB 侧提供: ~/reading-browser/supabase/sql/cleanup-lemma-v26-2026-08-18.sql软删而非硬删 —— 墓碑是给对端设备看的;硬删后没重装的设备下次 push 会把行重新传回云端)。

RVH 侧待办(本次未做,需在云端清理执行后进行):在设备上复验生词本里 rath / alway / lat / ye / opus 等词确实消失。


§10 知情取舍(不要重复劳动)

以下三项本轮故意不做,均已在交接单说明,记此防后续会话误当遗漏去"修":

  1. 预装库 91 个死 headwordalway rath lat ye opus vega …)仍在 vocabulary 表, 只是不再有任何 surface 归一到它们 = 不可达死行;反过来 always / rather 等正确词形 暂无本地词条,查询走 Supabase 缓冲池兜底(联网、首查稍慢)。

    ⚠️ 2026-08-25 订正:本条原写「两者都在下次整库 reseed 时自然收敛」—— 该说法只对全新安装成立。预装库导入是只增不减的(app_database.dart 里 唯一的 DELETE 只清 source='backfilled' 的行,预装行无删除路径),所以 reseed 即使 把死词条从新库拿掉,存量安装照样留着。 「缺失的正确词形」那一半会随 UPSERT 补上(纯新增),但「死词条消失」那一半不会。 详见 backlog「P2 — 预装库 prune 走不通」(含 FK RESTRICT 约束与两条出路)。

    这也是 P1(OCR 模糊匹配把 always 改回 alway另一半没能根治的原因 —— 纠错器盯上的正是这些还躺在库里的死词条。

  2. 7 条 POS 歧义映射bit→bite · ground→grind · clad→clothe · inbred→inbreed · capita→caput · bored→bor · routed→rout)—— 两读都成立需上下文,而 Layer 1 是无上下文查表, 属架构限制不是漏扫。(bored 另有幂等性理由:bore 自身映射到 bear。)

  3. patch_lemmatizer_assets.py 已由 RB 删除 —— 它路径停在迁仓前且断言「原串恰好出现 1 次」, v18 应用后必然报错,且是与 build_dict.rs 并行的第二本账。修正层单一真相源 = ~/reading-browser/src-tauri/examples/build_dict.rs §2.4 corrections / §2.5 force_as_base。 RVH 仓内两处指向它的文档(CLAUDE.md 红线 #5e v18 段、cross-end doc 13)已改指 build_dict.rs