主题
13 · lemmatizer 资产折叠进 reading_vocab.db reseed 交接(RVH → RB)
RVH 段二交付:接 RB 表契约
~/reading-browser/docs/cross-end/13-lemmatizer-fold-handoff.md, 把surface_to_base.json+base_forms.json两个打包资产烤进reading_vocab.db的三张lemma_*表,RVH 运行时改从 db 读 lemmatizer Layer 1/2,停止打包 JSON。 日期:2026-06-15 · RVH 主库 Schema 不变 / 预装库 新增三表(vocabulary 内容 byte-identical) · 优先级:架构收敛(打包/加载资产 3→1,消灭一类红线 #9 漂移)
⚠️ 行数/SHA 已被 v26 取代(2026-08-18):
lemma_*表的表契约(DDL)不变, 但行数会随每次资产修正变动(v26 140,281/101,709 → v27 140,277/101,713 → …)。 当前权威值只在根目录CLAUDE.md红线 #5e 维护,本文不再跟随 —— 这几个数字 在 v23/v24/v26/v27 已反复漂移,就地重写只会再漂一次。 (RBbuild_dict.rs修正 178 个 surface 的 AGID 古语/残根错映射)。 当前权威值见根目录CLAUDE.md红线 #5e;本轮落地见24-rb-lemmatizer-asset-v26-reseed-handoff.md(RB 交接)+25-rvh-lemmatizer-v26-confirmation.md(RVH 回执)。 本文 §1 的契约数字已就地更新;§3 验证回执保留 2026-06-15 当时的实测值不改 (那是真机日志与当时的 db SHA,属 as-built 记录)。
TL;DR(RB 要做的事 —— 全在 RB 会话,本文件只对齐契约 & 报新 SHA)
- 先放宽
scripts/sync-rvh-vocabulary.sh断言(TABLE_COUNT -eq 1→ 允许 4 表白名单) → 再跑同步脚本拉新 db(顺序关键,否则脚本因「表数 ≠ 1」中止)。 - 拉到的
reading_vocab.db新 SHA =05facc9bbdadcaccadd3b3a32a1e3c03e42df10485ece5922d352f19c8f249e6(旧c81d137492…)。bumpVOCABULARY_SEED_VERSION触发装机用户重灌。 - 改
src-tauri/src/commands/lemmatizer.rs:两个LazyLock+include_str!→ 从 attach 的 reading_vocab.db 一次性 SELECT 进内存(OnceCell<HashMap>/OnceCell<HashSet>, app setup eager init)。查词路径不变(仍内存 O(1))。去掉两处include_str!。 - 确认
helpers.rsreseed merge SQL 仍只 ATTACH 读vocabulary表;三张lemma_*表是 lemmatizer 运行时直接读的,不进 notebook merge 流程。 - 收口写
14-rb-lemmatizer-fold-confirmation.md。
RVH 不动 RB 仓库任何文件(段三由 RB 会话做)。
§1 表契约回执(按 RB §1 编码,已验证)
reading_vocab.db 新增三张表与现有 vocabulary 并存:
sql
CREATE TABLE lemma_surface_to_base (
surface TEXT PRIMARY KEY, -- 小写表面形
base TEXT NOT NULL -- 归一 base/lemma
); -- 行数见 CLAUDE.md 红线 #5e(← surface_to_base.json "map")
CREATE TABLE lemma_base_forms (
base TEXT PRIMARY KEY -- 小写 base 形
); -- 行数见 CLAUDE.md 红线 #5e(← base_forms.json "base")
CREATE TABLE lemma_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
); -- 4 行 provenancelemma_meta 实际内容(按 key 升序写入):
| key | value |
|---|---|
base_forms_count | 当前值见 CLAUDE.md 红线 #5e |
base_forms_version | agid-2016.01.19 |
surface_to_base_count | 当前值见 CLAUDE.md 红线 #5e |
surface_to_base_version | agid-2016.01.19 |
确定性纪律(RB §1):INSERT 按主键升序写入(surfaces..sort() / bases.toSet().toList()..sort()), generate_db 输出 run-to-run 可复现。bake 发生在修正层套用之后的 JSON 上(当时的载体是 patch_lemmatizer_assets.py;该脚本已于 2026-08-18 删除,修正层单一真相源改为 RB src-tauri/examples/build_dict.rs §2.4 corrections / §2.5 force_as_base): as 不在 lemma_surface_to_base(surface 无 as→a 映射)、as 在 lemma_base_forms(靠 base 自映射 返回 as)、picked→pick / passed→pass / trying→try 已修正。
§2 RVH 落地清单(已完成)
生成侧(pipeline)
tools/vocabulary_builder_v3/config.yaml:paths加lemmatizer_base_forms(lemmatizer_surface_to_base已存在)。tools/vocabulary_builder_v3/lib/db_generator.dart:DbGenerator加surfaceToBasePath/baseFormsPath构造参数;generate()在_verify(db)后调_bakeLemmaTables(db)(建三表 + 排序写入,沿用既有 sqlite3 写法,无时间戳/无 random)。tools/vocabulary_builder_v3/bin/generate_db.dart:从 config 取两路径透传给DbGenerator。- 不动:
patch_lemmatizer_assets.py(patch 在 bake 之前)、wordlist_builder.dart::_lemmatize(仍读 JSON 当 PK 归一输入)。
运行时侧(Flutter)
lib/core/nlp/lemmatizer.dart:加preloadFromMaps({baseForms, surfaceToBase})(从内存 Set/Map 直装, 绕过 JSON 反序列化)。preloadFromJsonStrings保留(CLI / 测试用)。lib/core/nlp/lemmatizer_flutter_loader.dart:loadLemmatizerFromAssets(rootBundle 读 JSON) →loadLemmatizerFromDatabase(从preinstalled_vocab.dbSELECT lemma_surface_to_base / lemma_base_forms喂preloadFromMaps)。复用预装库导入拷出的preinstalled_vocab.db;缺表自愈(探测无 lemma 表 → 从 bundled asset 重拷兜底)。lib/core/services/startup_initialization_service.dart:新增 Stage 1.5_initLemmatizer(), 排在_initDatabase()之后(串行)。启动顺序坑已解:lemmatizer 从main()并行批移走 → db 先于 lemmatizer 就绪。lib/main.dart:从 fast-initFuture.wait移除loadLemmatizerFromAssets()(四并行 → 三并行)+ 移除 import。pubspec.yaml:移除assets/nlp/打包;JSON 文件留盘(pipeline config 路径仍指向,作构建输入)。
§3 验证回执
| 项 | 结果 |
|---|---|
| db 表数 | 4(vocabulary + lemma_surface_to_base + lemma_base_forms + lemma_meta) |
| 行数 | vocabulary 18899(不变)/ lemma_surface_to_base 140370 / lemma_base_forms 101646 / lemma_meta 4 |
| db↔JSON 内容比对 | byte-equal(surface_to_base diff 0 / base_forms diff 0,全量逐行) |
| vocabulary 内容 | byte-identical(除 created_at/updated_at 外全列 dump SHA 一致) |
| lemmatize 抽查 | transferred→transfer / mice→mouse / picked→pick / passed→pass / trying→try;as 不在 surface 表、在 base 表 |
| lemma-regression.txt | 行为零变化(as→as base 层 / 其余 dictionary 层) |
| 设备运行时(Android 15,real device) | 启动序 Database initialized → Loaded from db: 140370 surface→base, 101646 base forms → Initialization completed in 2862ms,无异常。自愈兜底路径实测命中(既存安装旧 preinstalled_vocab.db 无 lemma 表 → 重拷成功) |
新预装库 SHA256:05facc9bbdadcaccadd3b3a32a1e3c03e42df10485ece5922d352f19c8f249e6 (旧 c81d137492bd4a20bc14fbb697ad441d6d5868b4531c7e92c12c50be76c0f4ba)。 output/baseline_sha256.txt 锚点已更新。
§4 两点 RVH 特有说明(RB 对齐用)
- RVH 主库 schema 不变、
_preinstalledVocabVersion不变:lemma_*表只在预装库里, RVH 运行时通过preinstalled_vocab.db(asset 的拷贝)直接读,不 merge 进 app 主库(RB 同理:lemma 表是 运行时直读,不进 notebook merge)。vocabulary 内容零变化,故 RVH 端无需 bump 预装版本触发 upsert; 既存安装由 loader 缺表自愈重拷处理(实测命中)。RB 端因 reseed 复制整包 db,仍需 bumpVOCABULARY_SEED_VERSION让装机用户拿到带新表的 db。 - 红线 #5e 措辞迁移:byte-equal 要求从「surface_to_base.json / base_forms.json 运行时资产」 转为「reading_vocab.db 内
lemma_*表」;JSON 降级为 pipeline 构建输入(仍需 RBbuild_dict↔ RVH 一致, 但不再是 shipped runtime 资产)。lemmatizer Layer 3/4 算法代码(后缀规则 + 裁决 + NFC)两端各一份的对齐义务照旧——折叠只合并数据,不合并代码。
关联
- RB 表契约源文档:
~/reading-browser/docs/cross-end/13-lemmatizer-fold-handoff.md - RB 收口(待 RB 会话写):
~/reading-browser/docs/cross-end/14-rb-lemmatizer-fold-confirmation.md - RVH CHANGELOG:本批
[Unreleased] / Changed - 红线:CLAUDE.md §跨端 Sync 协议红线 #5e(资产 byte-equal,措辞迁移见 §4.2)