Skip to content

27 · v26 之后的映射表残留复扫 —— 4 条同类漏网(RVH → RB)

物理仓库位置:~/reading_vocab_helper/docs/cross-end/27-rvh-lemmatizer-residual-scan-handoff.md 日期:2026-08-25 · 方向:RVH 复扫发现 → RB build_dict.rs(红线 #5e:RVH 纯消费,不能自己改资产) 上游:doc 24(RB v26 交接)· doc 25(RVH v26 回执) 数据文件:27-residual-band-31.tsv(截断带完整 31 条)


✅ 已交付并接收(2026-08-25 当日闭环)

  • RB 侧:commit fd3bfa6,4 条全走 §2.5 force_as_baseoverjoyed 这条 RVH 交给 RB 裁定,RB 判「修」,理由是 PARK 的判据是「两读都成立、 需 POS 上下文才能分」(bit/ground 那种架构限制),而 overjoyed 只有一读; 且 overjoy 连 33 万词频表都不在。RVH 认同。
  • 资产surface_to_base 140,281→140,277(−4)· base_forms 101,709→101,713(+4)
    • base_forms.json 8e3c993d9f57b361cff1c733f0602d942fbbbee408787291739f6ababaef6149
    • surface_to_base.json 69b0e498efa8a9fe9a7b584ef80b2bb458c14aaf1e4f776cb846ae7d8eb4cce6
    • 预装库 2880c504f86ed6728686afcf74ff5b52b0c4bc3d78cba4cfa45b837de872614f
  • RVH 侧:JSON fixture 同步 + _preinstalledVocabVersion 26→27 + 红线 #5e SHA 更新
    • 回归 v27-A/v27-B。跨端 phase0 两端当场重跑,lemma-regression(99) 与 改动面 + 同源 surface(12) 两组均 byte-identical
  • 真机验证(全新安装,v0→v27 路径):分享含 uninteresting / unconcerned 的句子, 两词均保持原样进入结果(不再变成 uninterest / unconcern)。

⚠️ 一条交接教训:RB 同步脚本把新 db 覆盖进 RVH 工作树时,RVH 会话正在做别的提交, git add -A 把这个 55MB 资产误并进了一条声称是 docs 的 commit55f909e)。 资产内容本身没问题,但提交信息不实。已由后续 commit 补正说明(不改写历史)。 下次跨仓同步资产时,接收侧应先 git status 确认改动来源再提交。


1. 先说结论:v26 修表这条路是对的,高频段没有漏网

用 v26 当时的撒网条件(surface 排名 ≤20000 ∧ base 排名 > surface×3)在修后资产上复扫, 165 条命中里几乎全是正确映射

people→person   data→datum      details→detail   based→base      women→woman
media→medium    criteria→criterion   alumni→alumnus   sat→sit    united→unit

外加已知 PARK 的 bit→bite / ground→grind。这印证了 doc 24 §4 的判断(机械规则分不开 真假阳性、必须人工裁定),也说明那轮裁定做得扎实。本交接单不是来翻案的。


2. 漏的是截断带(doc 24 §10 自己写明了)

频率排名 > 30000 的低频错映射 —— 撒网时按危害排序截断了。

20001–120000 这一段、且 base 是预装库词条(= 会进用户生词本 = 有实际危害):31 条。 其中大部分是对的fascinated→fascinate terrified→terrify overheard→overhearbabysitting→babysit schoolchildren→schoolchild spellbound→spellbind …)。

4 条需要修,与 v26 已修的 unbiased/outdated同一模式: 形容词被当成一个几乎不存在的动词/名词的屈折形,而那个 base 在预装库里还顶着正经 CEFR。

surface词频排名当前 basebase 在预装库里建议
uninteresting#53115uninterestB1 名词「Lack of interest; indifference」→ SELF(§2.5 force_as_base)
uninterested#71943uninterest同上→ SELF
unconcerned#68910unconcernB2 名词「Freedom from worry…」→ SELF
overjoyed#59425overjoyB1 动词→ SELF(边界情况,见下)

对照 v26 已修的同类(doc 24 §5.2「派生词而 base 为非词」那一组): unbiasedunbias(C2) · outdatedoutdate(C1) · unqualified · undersigned · unwilling。 一模一样,只是掉在阈值下面。

overjoyed 是边界情况,请 RB 自行裁定

overjoy真动词("To give great joy to"),overjoyed 作为其过去分词在语法上成立 —— 不同于 uninterest(名词,uninteresting 根本不是它的屈折形)。但现代英语里 overjoyed 几乎只作形容词。RVH 倾向修,但优先级低于前三条,若 RB 判「两读都成立」而归入 PARK, RVH 无异议。

危害量级

uninteresting / uninterested 是学习者读得到的普通词,uninterest 顶着 B1 —— 是 rath 陷阱的缩小版:危害同型,量级小一档rather 排名 ~1000 vs 这几个 5–7 万)。 不紧急,建议随下一轮 build_dict 改动搭车,不必为它单独发一次资产。


3. ⚠️ 修表治不了 RVH 侧那个 P1(两件事容易混)

① 归一错   always --Layer1 表写错--> alway    → v26 修表,已根治 ✅
② 纠错器错 always --表已修对--> always
                  --本地 vocabulary 无此词条--> 判为 unknown
                  --OcrFuzzyMatcher 编辑距离 1--> alway   ❌ 仍在

②里归一是对的,错在下游的 OCR 拼写纠错器:v26 只重烤了 lemma_* 没动 vocabulary (知情取舍),于是 alway 作为死 headword 还在、always 反而没有本地词条 → 成了"查不到的词" → 落进纠错器 → 编辑距离 1 命中 alway。RVH 实测 89 个 SELF 词里 36 个会被这样改写, 其中 27 个改回 v26 刚删掉的死词条

这条 RVH 自己修(把模糊匹配限定在 OCR 来源),已在 RVH backlog 立 P1,不需要 RB 动。 写在这里只是提醒:别以为表修干净了这个症状就没了,两者独立。

根治②的另一半在 RB:下次整库 reseed 时 prune 掉那 91 个死 headword + 补上正确词形词条。 已在 doc 25 §10 记为知情取舍,本单不重复立项。


4. 给 RB 的阈值建议(已验算,别用错口径)

  • 不要用「base 是预装库词条」单条件当危害边界 —— 实测命中 21,249 条,人工过不完。 (RVH 起初也想这么建议,验算后否决,记在这里免得 RB 重走一遍)
  • ✅ 用「频率反转 ∧ base 是预装库词条」组合条件,surface 排名上限从 30000 放宽到 120000: 候选集 196 条(≤30000 时 179 条,即放宽只多 17 条),逐条人工过成本可接受
  • 再往下(>120000)的 surface 基本不是现代英语常用词,可继续截断

5. 可复现扫描脚本

在 RVH 仓根目录跑(依赖 RB 的词频表 + RVH 的预装库;两者都是双端共享资产):

python
import json, sqlite3
FREQ = '/Users/larry/reading-browser/tools/vocabulary_builder_v3/data/google_word_freq.txt'
rank = {}
for i, l in enumerate(open(FREQ, encoding='utf-8', errors='ignore')):
    p = l.split()
    if p and p[0].strip().lower() not in rank:
        rank[p[0].strip().lower()] = i + 1

sb = json.load(open('assets/nlp/surface_to_base.json'))['map']
con = sqlite3.connect('assets/databases/lampio_dict.db')
vocab = {w: c for w, c in con.execute(
    "SELECT word, primary_cefr_level FROM vocabulary")}

SR_MAX, MULT = 120000, 3          # 放宽后的阈值
hits = []
for s, b in sb.items():
    rs = rank.get(s)
    if not rs or rs > SR_MAX:
        continue
    rb = rank.get(b)
    if (rb is None or rb > rs * MULT) and b in vocab:   # 频率反转 ∧ base 是词条
        hits.append((rs, s, b, rb, vocab[b]))
for rs, s, b, rb, cefr in sorted(hits):
    print(f"#{rs:<7} {s:18}{b:16} (base 排名 {rb or 'NOT_IN_FREQ'}, CEFR {cefr})")

判据沿用 doc 24 §4 第二层:

= base 在现代英语里不是该 surface 的词形(截断残根 / 古语 / 另一个词) 不修 = base 是同一词的规范词典形 搁置 = 两读都成立、需 POS 上下文(Layer 1 无上下文,架构上做不到)

⚠️ 别忘 doc 24 §4 第三层的完整性清扫:修一条就按 base 反查该残根下的全部 surface。 v26 首轮就是漏了 64 条同源映射(修了 changed→change 却留着 changing→chang)。

本单这 4 条的反查结果如下 —— ⚠️ 同源 surface 存在,但不是全都要改

base该 base 下的全部 surface处理
uninterestuninterested · uninteresting · uninterests前两个改 SELF;uninterests 不动(名词复数,映射正确)
unconcernunconcerned · unconcerning · unconcerns只改 unconcerned;后两个映射正确,不动
overjoyoverjoyed · overjoying · overjoys只改 overjoyed(若裁定改);后两个是规则动词形,正确,不动

即这三个 base 本身仍是合法词条,只是不该收编那几个形容词。改完后 uninterest/unconcern/overjoy 继续作为 base 存在、继续接收各自的名词复数/动词屈折 —— 不是把整个 base 铲掉。

订正:本单初稿曾写「这几个 base 下只有那 1–2 个 surface,无同源遗漏」,该说法不成立 (实际每个 base 下都有 3 个 surface)。结论不变(无需连带修改),但事实陈述已更正为上表。

⚠️ SELF 组按 doc 24 §2.1 必须两步(删 surface_to_base 映射 + 补进 base_forms), 只删映射会被 Layer 3 后缀规则推回来。这 4 个词实测都不在 base_formsuninteresting / uninterested / unconcerned / overjoyed 均为 False), 所以四条全都需要走 §2.5 force_as_base(它本就是"删映射 + 补 base"一起做)。

改完请照 doc 24 §7 的做法验证:lemmatize() 结果 + layer == "base" 一起断言 —— 只断言结果的话,漏补 base_forms 的复发能蒙混过关。


6. RVH 侧要做的

  • 无。资产改动由 RB 单点产出,RVH 走 /preinstalled-db-update 整包接收。
  • 若本轮只改这 4 条、vocabulary 表不动,RVH 侧仍需 bump _preinstalledVocabVersion (理由见 doc 25 §7:RVH 读 documents 目录的拷贝,不 bump 则存量用户拿不到)。