主题
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 | 🔴 预装库版本 bump | lib/shared/data/database/app_database.dart:211 | _preinstalledVocabVersion 25 → 26,并补 v26 版本注释日志 |
| 2 | 预装库整包接收 | assets/databases/lampio_dict.db | RB 会话已覆盖,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-19 | 140,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:175 | v54 as-built 条目不改,追加 v26 取代说明 |
| 9 | 回归用例 +5 | test/core/nlp/lemmatizer_dictionary_test.dart | v26-A…v26-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.md、 docs/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-A … v26-E)
按交接单 §2.1 的要求,自映射组连 layer == 'base' 一起断言:
| 用例 | 内容 |
|---|---|
v26-A | SELF 组 12 词(rather latter his outer always sometimes inner us during yes opera clothes)自映射 且 layer == base |
v26-B | REMAP 组 9 条(based→base · changed/changing→change · raised/raises→raise · tries→try · latest→late · routing→route · batteries→battery) |
v26-C | KEEP 哨兵 5 条(people→person · could→can · its→it · better→good · longer→long)不得被误伤 |
v26-D | 修正后的词仍满足 normalize 幂等性 |
v26-E | rath 仍在 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-validatedLayer 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,dictionaryrb.csv 与 rvh.csv 彼此仍逐字节相同(两端一起陈旧),所以交接单那句复验按字面执行会通过, 但它证明的是「四个月前两端一致」,不是「今天两端一致」。这组文件已不能作为 v26 的验收证据。
6.2 实际做的复验:两端当场重跑再 diff
| 输入集 | 词数 | RB(cargo run --example phase0_normalize --release) vs RVH(dart build cli) |
|---|---|---|
test/data/lemma-regression.txt(红线 #5e 指定) | 99 | ✅ byte-identical |
docs/cross-end/phase0_inputs.txt(两端均以 v26 资产重跑) | 131 | ✅ byte-identical |
| 本轮改动的 178 个 surface(最强检验) | 178 | ✅ byte-identical |
第三行是交接单没要求、但真正对准改动面的检验:直接 diff 新旧资产得出受影响 surface 全集 (removed 89 + remapped 89 = 178,base_forms +63/−0,与 §3/§5 数字逐一吻合),两端跑完全等。
交接单「178 个改动 surface 与 phase0 输入词零交集」的说法经复核成立(交集为空集, lemma-regression.txt 同样为空)—— CSV 对不上是另一个独立原因。
遗留(RB 仓,非 RVH 可改):✅ 已处置(2026-08-24,方案 A):两个 csv 已用两端当前实现重新生成(输入不变,131 词),docs/cross-end/{rb,rvh}.csv+phase0_inputs.txt建议重新生成cmp逐字节相同;RBdocs/cross-end/README.md数据资产行补了「基线时点 2026-08-24 + layer 词表dictionary/base/fallback(+empty)」作新鲜度指纹。经过与勘误详见 RBdocs/cross-end/26-rvh-phase0-evidence-stale-report.md(含顶部处置回执)。 同批把本文 §6.3 记的遗留项也修了 —— RVHCLAUDE.md红线 #5e 的验证命令段dart compile exe→dart 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:487的writeAsBytes,而:476if (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) |
vocabulary 表 | 18,898 行未动 ✅ |
| 跨端 phase0 diff | ✅ 三个输入集全部 byte-identical(§6.2) |
§9 存量数据清理(RVH 侧代码零改动)
按交接单 §9.1b 裁定:不做本地迁移、不落 migration(存量用户全是会重装的内部内测用户)。
⚠️ 但重装清不掉 —— 这些行同时在 Supabase 的 user_* 表里,重装重登后 sync pull 会原样拉回, 且拉得进来(本轮没动 vocabulary,rath 仍是预装库 headword,父行/FK 存在性检查照样放行)。
清理在云端做一次即可、两端共用,SQL 由 RB 侧提供: ~/reading-browser/supabase/sql/cleanup-lemma-v26-2026-08-18.sql(软删而非硬删 —— 墓碑是给对端设备看的;硬删后没重装的设备下次 push 会把行重新传回云端)。
RVH 侧待办(本次未做,需在云端清理执行后进行):在设备上复验生词本里 rath / alway / lat / ye / opus 等词确实消失。
§10 知情取舍(不要重复劳动)
以下三项本轮故意不做,均已在交接单说明,记此防后续会话误当遗漏去"修":
预装库 91 个死 headword(
alwayrathlatyeopusvega…)仍在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)另一半没能根治的原因 —— 纠错器盯上的正是这些还躺在库里的死词条。7 条 POS 歧义映射(
bit→bite ·ground→grind ·clad→clothe ·inbred→inbreed ·capita→caput ·bored→bor ·routed→rout)—— 两读都成立需上下文,而 Layer 1 是无上下文查表, 属架构限制不是漏扫。(bored另有幂等性理由:bore自身映射到bear。)patch_lemmatizer_assets.py已由 RB 删除 —— 它路径停在迁仓前且断言「原串恰好出现 1 次」, v18 应用后必然报错,且是与build_dict.rs并行的第二本账。修正层单一真相源 =~/reading-browser/src-tauri/examples/build_dict.rs§2.4corrections/ §2.5force_as_base。 RVH 仓内两处指向它的文档(CLAUDE.md 红线 #5e v18 段、cross-end doc 13)已改指build_dict.rs。