Skip to content

短语自动高亮 thin-slice(高精度薄片 · v1 验证)

物理仓库:RB(/Users/larry/reading-browser)。纯 RB 侧,不动 RVH / Supabase / 预装库。 落地后请把本文件复制到 docs/plans/archive/phrase-auto-highlight-thinslice-plan.md(CLAUDE.md §8 计划持久化)。

Context(为什么做这个)

现状:阅读页里短语(phrasal verb / idiom)的发现需要 2 个动作(选中文本 → 点"短语"按钮),绝大多数用户根本不知道有这功能。功能初衷是解决"词都认识、合起来傻眼"——即暴露非组合性多词单位。

只读评估(见会话)暴露两个问题:

  • 精度:检测是 context-blind 的 surface 匹配,kind of(basic) 在 "a special kind of" 里被误标。按真实出现次数,约 35% 命中落在 basic 噪声桶a number of / after all / as long as)。已核实 excluded_words零短语,文档"决定 A(basic 经 excluded_words 默认排除)"实际未落地,noise 确实露给用户。
  • 召回:单节点 matcher 漏标率和生词高亮密度强耦合(3.9% → 76%),用户学得越多短语越不准。

本 thin-slice 是最小代价验证:"只自动露出高精度短语,用户到底点不点"。两个白捡的设计红利:

  1. 精度:用现成的 basic tag 当粗过滤,只自动高亮非-basic 的 idiom + phrasal verb,砍掉 35% 噪声桶。basic 过滤只放自动路径,选区按钮路径仍匹配全部短语 → 保留"决定 A"的用户捞回意图。
  2. 召回:把短语扫描插在 highlightVocabulary 之前(文本节点还没被生词高亮切碎时跑)→ 绕过 3.8%→76% 的自伤切断,不写跨节点 matcher 就拿到近乎满召回,只剩 ~0.1% 内联标记切断。代价是给 vocab/discovery 的 TreeWalker 加"跳过 .rb-phrase 内部"。

明确不做(验证通过后才进的后续相):跨节点 block-flatten matcher、RVH pipeline 的"非组合性三档标签"、整页批量 LLM 消歧。

用户已确认的决策

  • 自动高亮范围 = 非-basic(is_idiom OR is_phrasal_verb,且非 basic)
  • 验证埋点 = 新建 local-only 小表,可用 rb_db_query 直接算 CTR / 误报率。

改动清单

A. Rust:让 matcher 带回 is_basic(不在 Rust 硬筛,交给前端按路径决定)

src-tauri/src/commands/vocabulary/query.rs · match_phrases_in_node_texts + match_phrases_in_text

  • SQL 增 MAX(CASE WHEN je.value='basic' THEN 1 ELSE 0 END) AS is_basic
  • phrase_map value (bool,bool)(bool,bool,bool)(加 is_basic)。
  • match_phrases_in_text 命中时把 is_basic 写进 PhraseMatch

src-tauri/src/commands/vocabulary/mod.rs · PhraseMatch struct:加 pub is_basic: bool。 → 选区路径(highlightPhrasesInSelection)拿到该字段但不读,行为不变;自动路径据此过滤。

B. Rust:local-only 埋点表 + 命令(migration v20)

src-tauri/src/db/migrations.rs · 新增 v20(migration-only,不动 baseline,沿 v11 pronunciation_attempts / v16 word_cloze_contexts precedent):

sql
CREATE TABLE phrase_interaction_log (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT,
  action TEXT NOT NULL,         -- 'shown' | 'clicked' | 'dismissed'
  phrase TEXT,                  -- clicked/dismissed 时填;shown 为 NULL
  count INTEGER,                -- shown 时 = 本页自动高亮短语数;其余 NULL
  page_url TEXT,
  created_at TEXT NOT NULL
);
  • local-only,不进 Supabase 同步矩阵(在 docs/database-schema.md 注明)。
  • 含 user 活动 → 加进 clear_learning_data_if_user_changed(红线 5c,沿 word_cloze_contexts 先例)。

新命令 log_phrase_interaction(db, action, phrase, page_url, count)(放 query.rs,DB 写入类),lib.rs generate_handler! 注册。

C. content-script:新全页短语扫描函数

src-tauri/src/content-script/features/phrase-highlight.js · 新 highlightPhrasesOnPage()

  • 复用 collectRangeTextNodes 的 acceptNode 过滤集(已跳 script/style/textarea + .rb-highlight/.rb-phrase/.rb-popup-root/mark.rb-mark),但 TreeWalker 作用域改 document.body、去掉 range 相关判断。
  • 一次 invoke('match_phrases_in_node_texts', { texts, maxFrequencyRank:null })(build phrase_map 一次,高效)。
  • withObserverPaused + 分批(沿 highlightVocabulary 的 100 节点/批 + await setTimeout(0)):对每节点 perNode[i] .filter(m => !m.is_basic) 后,复用 wrapPhrasesInTextNode(无 clip,offset 直接用 char_start/char_end;sentence 传 node.textContent 作上下文,供点击侧 explain_phrase)。
  • 累计 total;invoke('log_phrase_interaction', { action:'shown', count: total, page_url: location.href })
  • 入口受 state.settings.phraseAutoHighlight 门控。

D. content-script:插入 pipeline + 防嵌套

src-tauri/src/content-script/index.js · full-pipeline(初始 L72-90 MutationObserver 重跑 L149-156 两处):

js
await loadAndWrapAnnotations();
if (state.settings.phraseAutoHighlight) await highlightPhrasesOnPage();  // ← vocab 之前
await highlightVocabulary();
await highlightDiscoverable();

防嵌套(vocab/discovery 不钻进 .rb-phrase 内部,否则 .rb-phrase 里套 .rb-highlight):

  • src-tauri/src/content-script/features/highlight.js:TreeWalker acceptNode highlightTextNode 各加一行 if (p.closest && p.closest('.rb-phrase')) return REJECT/return;
  • src-tauri/src/content-script/features/discovery.js:同款 TreeWalker,加同样跳过。 → 效果:短语内的 B1/B2 词不再单独染色,习语作为整体单位呈现(视觉更对)。

E. content-script:埋点接线(复用现有链路,不改 events.js/popup.js 逻辑本体)

  • 点击:events.js 现有 .rb-phrase click handler 里,进入 handlePhraseLookup 前加一发 log_phrase_interaction({action:'clicked', phrase})
  • 误报:popup.js dismissPhraseHighlight(LLM fit===false 路径)里加一发 log_phrase_interaction({action:'dismissed', phrase})。 → CTR = clicked / Σshown.count;误报率 = dismissed / clicked(精度真实信号)。

F. settings 开关

  • src-tauri/src/content-script/features/settings.jsBATCH_KEYS'phrase_auto_highlight'loadSettings()state.settings.phraseAutoHighlight = batch.phrase_auto_highlight === 'true'
  • src-tauri/src/content-script/core/state.js:settings 对象加 phraseAutoHighlight: false
  • React Settings 页:在 discovery_enabled toggle 旁加一个"自动高亮短语"开关,写 phrase_auto_highlight key(复用现有 toggle pattern;JSX/aria 文案走 src/lib/strings/,红线 S19)。
  • 验证窗口默认 ON:在 init_data.sql settings 段 seed phrase_auto_highlight='true'(仅为攒数据);任何外部/正式发版前按验证结论再定默认值。

不碰 / 兼容性

  • 选区"短语"按钮路径(toolbar.js handleSelectionPhraseshighlightPhrasesInSelection零改动:仍匹配全部短语(含 basic),捞回能力不丢。
  • 点击消歧链路(handlePhraseLookup/maybeResolvePhrase/explain_phrase,受 contextDisambiguation 开关)原样复用
  • 样式 styles.js.rb-phrase-pv/.rb-phrase-idiom(靛蓝/紫波浪线)已完备,不改。
  • 红线核对:#9 normalize 已在 matcher 内(normalize_phrase);local-only 表不进同步矩阵;无 hex;不新增 webview 协调 useEffect(架构原则 1/2)。

验证(端到端)

  1. /build-check(cargo check + pnpm build)。删本地 dev DB 重跑触发 v20 migration。
  2. pnpm tauri dev,开一篇真实长文(如 Wikipedia / Sherlock 文本):
    • 加载完成后非-basic idiom/PV 自动出波浪下划线;kind of/a number of/after all(basic)被自动标。
    • rb_dom('.rb-phrase') 抽查:.rb-phrase嵌套 .rb-highlight
    • rb_logs(tail=20) 无报错。
  3. 点一个自动高亮短语:popup 正常 + explain_phrase 消歧(fit=false 时下划线消失)。
  4. rb_db_querySELECT action, COUNT(*) FROM phrase_interaction_log GROUP BY action; 应见 shown / clicked / dismissed 行。
  5. Settings 关掉开关 → reload → 无自动高亮;选区按钮仍工作(含 basic)。
  6. 验证指标:真实读几篇后,rb_db_query 算 CTR = clicked / Σshown.count、误报率 = dismissed / clicked。判据:CTR 是否说明"自动露出硬短语"有真实engagement;误报率反推 basic 过滤够不够。
  7. /arch-check + /code-review + /doc-sync-check(更新 database-schema.md 加 v20 表 + local-only 标注;migrations 版本表加 v20 行)。

后续相(验证通过才启动,不在本 plan

  • RVH pipeline 产出"非组合性三档标签"(always-idiomatic / context-dependent / mostly-literal)替代粗糙的 basic 代理 → 跨端新会话(CLAUDE.md §9)。
  • 跨节点 block-flatten matcher(治 ~0.1% 内联切断 + 选区路径召回)。
  • context-dependent 桶的整页批量 LLM 消歧(并入共享 llm_call_log 成本看板)。

v2 后续收敛(2026-06-24,逐步收敛 · 不求一次到位)

v21 已落地「非组合性三档」(always/context/literal),自动路径只露 always(根治 v1 41% 误报)。 本轮基于真实埋点数据,逐步收掉残余误报。问题一步步解决,每步独立验证。

埋点诊断(all-v21 数据,clean)

phrase_interaction_log 拉数(2026-06-18 v21 之后,无 v20 旧数据混入):

指标数值
密度489 实例 / 221 页 ≈ 2.2 个/页(克制)
CTR25 / 489 = 5.1%
误报率(dismissed/clicked)7 / 25 = 28%
误报率(dismissed/shown)7 / 489 = 1.4%

7 次 dismiss 拆根因 → 两桶,分别解:

  • 桶① boilerplate artifact(2/7)all right reserve ×2 = 版权页脚 "All Rights Reserved" 被 lemma 折叠误命中。与 idiomaticity 无关,是扫描范围问题。→ 本轮第 0 步解决
  • 桶② 真·instance-level 歧义(5/7)to die for ×2 / in the black ×2 / so much for ×1,全是「always 档习语在具体句里恰好字面义」。类型层静态分档修不掉,只有带上下文判断能解。→ 留下一步

✅ 第 0 步:自动路径扫描范围收窄到主内容容器(2026-06-24 完成 + 验证)

为什么不绑 reader 模式(用户初始想法):① auto-reader 按域名 opt-in、默认关 → 绝大多数浏览在 live 模式,绑 reader 会让短语高亮在多数场景消失;② 现状 phrase 恰恰只在 live 显示、reader overlay 只重刷 vocab 不刷 phrase → 绑 reader 是「搬家 + 反转」非简化。改用 live DOM 扫描范围收窄,不丢覆盖、不搬家。

改动(仅 1 源文件 + 重建 bundle):

  • src-tauri/src/content-script/features/phrase-highlight.js
    • 新增 findMainContentRoot():按语义容器清单(article / [role=main] / main / .article-content / .entry-content / .story-body / #article-body / .article-body / .content-body / .post-content,与 reader.js extractWithHeuristic 对齐)找第一个正文字数 ≥ 200MAIN_CONTENT_MIN_CHARS)的容器;找不到退回 document.body(安全侧,行为同旧)。
    • collectBodyTextNodes() 的 TreeWalker 根从 document.body 改为 findMainContentRoot()
    • 只影响自动路径:选区「短语」按钮走 collectRangeTextNodes,不动,全档捞回保留。
  • node scripts/build-content-script.mjs 重建 content-script.js bundle。

效果:页脚 / 导航 / 侧栏 / 广告里的 boilerplate 从根上不进扫描范围——一刀切掉整个 chrome 噪声类(不只 footer),零延迟零成本。

验证(dev app 实机,rb-debug)

  • 测试页 The Happy Prince(americanliterature.com,idiom 丰富)。
  • 全文档 .rb-phrase[data-rb-phrase-auto] 计数 = 1,且在故事正文(idiom out loud)。
  • 同页受控对比(铁证):<footer class="footer-nav"> 内 vocab rb-discover 有 2 个(vocab 仍扫全 body,证明 footer 可达)、phrase .rb-phrase 0 个(phrase 已被限制在正文容器内)。旧逻辑下两者共用同一 document.body,vocab 能到 footer 则旧 phrase 必能到 → 现已分道,收窄生效。

边界:本步只解决桶①。正文内桶②(真歧义)原样保留,留下一步。

⏭ 下一步:桶② 正文内真歧义(A→B 流水线 · 决策已敲定 2026-06-24)

已确认数据(2026-06-24 查 dev 库 vocabulary.word_tags):3 个误报短语全部 idiomaticity:always

  • in the black ["idiom","idiomaticity:always"]
  • so much for ["idiom","idiomaticity:always"]
  • to die for ["idiom","idiomaticity:always"]

埋点实时核实(2026-06-24,dev 库 phrase_interaction_log):clicked=25 / dismissed=7 / Σshown.count=639 → CTR 3.9%(25/639)、误报率 28%(7/25)。clicks 自上次记录卡在 25 无增长。dismiss 拆桶确认无新惯犯:to die for×2 / in the black×2 / all right reserve×2(桶① boilerplate,第 0 步已解)/ so much for×1。 → 28% 误报 100% 可归因到两个确定性根因(桶① boilerplate + 桶② 这 3 个 mistag),无任何弥散性误报。这是「A 不必等 gate-A 数据」的硬依据。

问题:always 是 type-level 标签(词条整体非组合性),但这 3 个各有常见字面读法,surface 匹配器分不出 → 字面出现被误标:

  • so much for"thank you so much for…"(极高频,碾压性字面搭配)
  • to die for ← "to die for [a cause]"(为某事而死)
  • in the black ← "in the black [coat/car]"(black 作形容词)

根因落在「静态 vs 运行时选址轴」:① 经常字面 = type-level 标错(静态可修);② 通常习语·偶尔字面 = instance-level 残留(只有上下文判断能解)。

rubric 系统性盲区(必须带给 RVH,否则下次 reseed 打回):RVH judge 问的是"这是不是真习语?"(这 3 个答案都是 yes → 给了 ≳90% → always),却没充分加权"这个 surface 串在真实语料里习语义占比多少"so much for / to die for 都是「高频字面搭配 + 真习语」双重身份串——这是 always-mistag 的系统性陷阱类型,不是 3 个孤例。给 RVH 的核心指令 = judge prompt 显式估计 surface 串的字面搭配频率,识别这类陷阱串。

方案 = A 喂 B 的一条流水线(不是二选一)

方案 A:RVH 预装库把 over-confident 的 always 重判降级

方案 B(= backlog #3「§9 深度短语模式」):context 桶逐句 LLM 二分类,判 true 才高亮

已敲定决策(2026-06-24)

决策点结论
A 启动时机立即动手,解耦 gate-A。A 是类型层标错的根因修复,不依赖数据量;gate-A 留着测 A 之后的干净数据。
in the black 档位context(财经习语真有价值,深度模式能救回)
to die for 档位context("a dress to die for" 习语本身常见)
so much for 档位literal("thank you so much for" 字面碾压、习语义罕见到不值得占 LLM 配额;落 literal = 深度模式也不送,"so much for our plans" 永不救回,有意取舍)
第 0 步提交✅ 已提交 commit 2a2e0b3(2026-06-24 16:15,本节"代码未提交"旧述作废)
  • A(RVH 新会话,§9):① 先查 RVH pipeline 按什么规则把这 3 个判成 always(避免下次 reseed 打回);② 修 rubric 的「字面搭配频率」盲区;③ 重判 in the black/to die for→context、so much for→literal;④ additive reseed(word_tags tag 值,无 migration)。详细交接见 phrase-noncompositionality-tiering-crossend-plan.md §10
  • B(RB 单开会话,gate-A 重测达标后):复用 backlog #3 两段式(本地框候选 → 含候选句打包送 flash → 判 true 才画),目标桶 = context(含 A 降级进来的 in the black/to die for)。literal(含 so much for)不送 LLM
  • 收益:always 桶维持本地秒出(默认体验零延迟/零成本,只留真习语);context 歧义短语挪进 opt-in LLM 通道、in the black 等真习语义照样救回(财经句判 idiomatic→高亮,"black coat"判 literal→不高亮)。
  • 不采用「always 档加载期 LLM 后台回验」:全 App 最高频 LLM 触发点 + 卡加载路径 + 先画后撤跳变,ROI 倒挂。

关键次序洞见A + reseed 单独就能闭合 gate-A —— 这 3 个移出 always 后立即停止自动高亮,默认层误报从 28% 掉到接近 0 ⚠️ 此预测已被 2026-06-25 gate-A 实测推翻(见下方「gate-A 实测」节):v22 已 demote 222 个,但残余仍 28% 误报,来自别的 surface 陷阱 + lemma 折叠短语。A+v22 reseed 没有闭合 gate-A。B(§9)仍不是 default-on 发版的前置,是 always-only 默认层之上、把 context 桶习语义经 opt-in LLM 救回的独立增强。

落地次序(已敲定)

① 第 0 步 commit 2a2e0b3 ✅(桶① boilerplate 根治,已提交)

② RVH 新会话:A = 重判 3 个 + 审 rubric 字面搭配频率盲区 → additive reseed → 交接确认文档

③ RB 会话:/vocab-reseed 整包覆盖(机械、快)→ 3 个移出 always → gate-A 重测 → 误报应 <10% → 评估 phrase_auto_highlight 默认 on
       ↓(独立、不阻塞发版)
④ RB 单开会话:B = §9 深度短语模式(context 桶 opt-in flash 消歧救回)

② A 必须 RVH 会话(Flutter/Dart + pipeline,§9 隔离);④ B 必须 RB 单开会话。③ reseed 建议独立先做(单独就兑现 gate-A 精度提升),也可并进 ④ 开头。

顺带观察 → 已记 backlog

vocab 高亮仍扫全 body(验证时 footer 里有 rb-discover)。用户倾向也收窄 vocab。独立项,已记入 docs/plans/backlog.md,单独评估/会话推进。


gate-A 实测(2026-06-25,v22 reseed 后)

方法变更:放弃「点击→自动 dismiss」做 FP 信号——实测发现 on-click LLM 兜底不可靠on the loose / in a fix 等明显误报点击后 15s+ 未被 dismiss,console 无 explain 痕迹),dismissed/clicked 会严重低估真实误报。改用 Claude 直接读 data-rb-sentence 逐条判习语 vs 字面(比 on-click deepseek 更强的 oracle,零 LLM、零等待)。 省 token 技巧phrase_interaction_logshown 行带 count = 该页 always 数 → 开页先查 SELECT count … WHERE page_url=… AND action='shown' ORDER BY id DESC LIMIT 1,count=0 直接跳过不走 DOM。

样本(10 篇真实文章,rb-debug 驱动)

体裁always 总数
文学叙事(Longreads ×2)28
科普带比喻(snexplores)14
思想随笔(Aeon/Psyche ×3 有产出)36
干硬体裁(IEEE科技 / Guardian政论×2 / Psyche指南)40

always 桶很稀疏:1.8 个/页;干硬体裁(科技/政论/政策/how-to)整篇 0 always(已用 shown count=0 + DB tag 核实 = 真稀疏,非回归——highlightPhrasesOnPage 跑了、这些体裁的习语多是 context 或不在库)。只有文学/叙事/思想随笔有产出

18 个 always 高亮的质量

类别占比
✅ 干净真习语1161%
⚠️ 正确但低价值(basic 噪声:no matter what / pay attention211%
❌ 硬误报(字面误命中)528%

干净实例:black gold · unsung hero · come out of the closet · at stake · want for nothing · piece of the puzzle · anything goes · pave the way · make do · pan out · at bay。

5 个硬 FP 的根因(双轨,已稳定)

短语句子(字面误命中)根因类
on the loose"grow crops on the loose grit"🟠 surface 陷阱
for the bird"matters... for the birds"(真鸟)🟠 surface 陷阱
to speak of"refuse to speak of the condition"🟠 surface 陷阱
in a fix"bond in a fixed proportion"(fixed→fix)🔴 lemma 过度折叠
all right reserve"All rights reserved"(rights→right,正文内嵌版权行)🔴 lemma 过度折叠

结论(喂下一轮决策)

  1. gate-A 未达标:v22 后 always-only 真实 FP ≈ 28%(目标 <10%)。A+v22 reseed 没闭合 gate-A(推翻上一节乐观预测)——demote 222 个之后,仍有长尾 surface 陷阱 + lemma 折叠没被覆盖。
  2. 修复路径不是 §9(§9 只救 context 桶,对这 5 个 always 误报零作用),而是两件 RVH 侧事,且都是 gate-A 必须项
    • idiomaticity retag 第 2 轮:surface 陷阱 on the loose/for the bird/to speak of(+ 同模式 re-scan)→ 降 context。与 v22 同机制(A→B 延续)。
    • lemmatizer 资产治理fixed→fix/rights→right 幻影匹配(backlog 已有条目,本次实测把它从"可选"升为 gate-A 阻塞项)。
  3. 低价值 basicno matter what/pay attention):非 FP 但噪声,可一并评估降 context。
  4. 覆盖侧:always 桶"又稀又脏"——多数文章 0-2 个、剩下 1/4 是误报。default-on 前两件 RVH 事都得做。

埋点数据:本次普查记录在 dev 库 phrase_interaction_log(v22 干净基线,用户测前已清表;clicked 数据因 on-click dismiss 不可靠、且我中途点击过几个,不作 FP 依据——FP 以上表 Claude 判定为准)。


gate-A 复测(2026-06-26,v23 reseed 后 — 两轨齐)

v23 合并版(track-① surface 陷阱 demote 249 条 always→context + track-② in a fix→context / all right reserve prune)reseed 后复测。同法:Claude 逐句读 data-rb-sentence 判习语 vs 字面,:not() 链枚举每页 always 高亮;文学/叙事/思想随笔体裁取样。SHA 747f4732…,运行库实测三档 always 2789 / context 3450 / literal 371。

样本(8 篇真实文章,rb-debug 驱动)

Aeon essays ×2(Zhuangzi / global-tax)· Psyche ×2(relationship-OCD 指南=0 / needing-others)· Longreads ×2(Living-in-an-Alive-World=Sibley 观鸟随笔 7 个 / Ramona-Ausubel 访谈)· Texas Highways ×2(Guadalupe 灾后 / Woodlands Mall=0)。

15 个 always 高亮的质量(13 distinct,at stake ×3)

类别distinctinstance占比(instance)
✅ 干净真习语91173%
⚠️ 正确但低价值(话语功能:no matter what/pay attention/for the most part3320%
❌ 硬误报(字面误命中)117%

干净实例:anything goes · pave the way · making do · panned out · at bay · at stake(×3) · want for nothing · piece of the puzzle · come up with。

唯一硬 FP(新发现的同类 surface 陷阱)

短语句子(字面误命中)根因类当前档
all along"All along the ravaged river, heavy equipment operators…"(沿河空间义)🟠 surface 陷阱(HOMONYM:空间"all along [地点]" vs 习语"自始至终")always

与 round-2 已治的 for the bird/to speak of 完全同类,只是 v23 re-scan 没扫到它 → 残留长尾。

结论

  1. gate-A 达标(卡边):always-only 真实硬 FP ≈ 7%(1/15 instance · 1/13 distinct),从 v22 的 28% → 7%,< 10% 目标。两轨修复在真机生效——上一轮 5 个硬 FP(on the loose/for the bird/to speak of/in a fix/all rights reserved全部消失;同句出现的 "for the birds" 不再被高亮(demote 验证)。
  2. 覆盖没砍过窄:尽管 v23 一次 demote 249 条,always 桶仍 1.9 个/页(≈ 上一轮 1.8);Sibley 观鸟随笔单篇 7 个。文学/叙事/思想随笔有产出,干硬体裁(指南/资讯)仍 0——真稀疏非回归。
  3. 残留两类(均非阻塞、已知修法)
    • all along 1 个硬 FP:HOMONYM surface 陷阱长尾 → 下一轮(可选 round-3)demote-only always→context,与 round-2 同机制。1 个词,便宜。
    • 3 个 ⚠️ 低价值话语no matter what/pay attention/for the most part):正是 §11.4 已记录的"纯话语功能降 literal"候选,非 FP、非阻塞。
  4. 默认 on 评估:gate 已过(7%<10%)→ 可评估 phrase_auto_highlight 默认 on 进发版。建议把 all along demote + §11.4 话语子集降 literal 作为一次廉价 round-3(demote-only、零风险)顺手收掉,把 FP 推向 ~0 + 清掉 ⚠️ 噪声,再/或并行 default-on。

埋点数据:dev 库 phrase_interaction_log 含 v22+v23 混合行(本轮 FP 不依赖埋点、以逐句 Claude 判定为准;shown.count 仅用于跳过 0-always 页)。