Carve:批注锚定是把读者的话绑在会变的文本上
/

Carve:批注锚定是把读者的话绑在会变的文本上

AIGC
2026年8月7日
约 17 分钟
暂无翻译稿。
此文章包含 AI 生成内容,请注意甄别。

批注系统的核心问题不是存储,而是锚定: 当被标注的段落被编辑、重排或删除, 读者的那句话还能不能回到它原来指向的位置?


carve author/runescope/meta/corpus

一次看起来只是 API 迁移的重构

最近两次 git sync(8 月 5 日、8 月 6 日) 包含了一批 annotation 组件的显著改动:

  • useGitHubDiscussions.ts:从 REST API 迁移到 GraphQL, 新增统一 graphql() 请求函数, findDiscussionByPage 返回值从 number 变为 {number, id}, 支持游标分页;
  • annotationFingerprint.tsfindAnchorInDOM 的返回类型 从 Range | null 变为 AnchorMatch, 新增 reason 字段区分三种失败原因;
  • AnnotationClient.vue:新增 pendingAnchorpendingRange, 修复选区在用户点击 textarea 后丢失的问题;
  • AnnotationPopover.vueAnnotationSidebar.vue: UI 层适配新的锚定诊断状态。

这些改动的表层是工程优化——更高效的查询、更好的错误处理。 但它们的深层改变是另一件事: 锚定从「找到/没找到」的二元结果,变成了带诊断的状态机。

W3C 的 TextQuoteSelector 与三段式指纹

W3C Web Annotation Data Model 定义了 TextQuoteSelector: 用 exact(选中文本)、prefix(前文片段)和 suffix(后文片段) 三段文本描述一个锚定位置。 这不是坐标,不是 XPath,而是内容本身。 (W3C Web Annotation Data Model, TextQuoteSelector)

这套方案的鲁棒性来自一个朴素的假设: 即使文档结构改变,只要被标注的文本及其上下文仍然存在, 锚定就能恢复。

Hypothesis 在 2015 年从 Annotator 项目的 XPath 方案 迁移到 text-quote anchoring, 正是因为 XPath 在 DOM 结构变化时脆弱得不可用。 他们的博文 Fuzzy Anchoring 描述了这个迁移: XPath 锚定依赖精确的 DOM 路径, 一旦页面结构调整(重渲染、组件化、SPA 路由切换), 所有锚定都会失效; text-quote 锚定只依赖文本内容,对 DOM 结构无感。

当前 annotation 系统的 AnnotationAnchor 结构:

typescript
interface AnnotationAnchor {
  selected: string // 选中的原文
  prefix: string // 选中文本前 N 个字符
  suffix: string // 选中文本后 N 个字符
  occurrence: number // 同一指纹在页面中第几次出现
}

这与 W3C TextQuoteSelector 的 exact / prefix / suffix 同构。 但多了一个 occurrence 字段。

occurrence:W3C 没有显式处理的消歧层

W3C 模型假设 exact + prefix + suffix 在文档中唯一。 大多数情况下成立——30 字符的 prefix 和 30 字符的 suffix 提供了足够的上下文窗口。 但在以下场景中,这个假设会失效:

  • 同一段话在页面中重复出现 (如模板化的警告框、重复的代码注释);
  • 短选区 + 高频词汇 (如选中「the」或「是」)。

occurrence 通过计数解决这个问题: 当同一 prefix + selected + suffix 组合在页面中出现多次时, 记录它第几次出现。 锚定时,只匹配第 occurrence 次出现的位置。

这不是 W3C 模型的一部分, 但它是 text-quote anchoring 在实际 DOM 中落地时 几乎必然需要的补充。

从二元到诊断:AnchorMatch 的三种失败原因

之前的 findAnchorInDOM 返回 Range | null。 null 意味着「锚定失败」——但为什么失败?

新版本的 AnchorMatch 把失败原因拆成三类:

typescript
interface AnchorMatch {
  range: Range | null
  reason: 'exact' | 'selected-missing' | 'context-mismatch'
}
  • exact:完整指纹匹配,selected 内容验证一致。 这是理想状态,可以高亮、可以交互。
  • selected-missing:selected 文本本身在页面中找不到了。 这意味着核心内容已变——段落被重写、删除或替换。 此时批注已经指向一个不存在的文本。
  • context-mismatch:selected 还在,但 prefix 或 suffix 对不上。 这意味着上下文被修改了—— 前后文有编辑,但选中的核心文本还在。 批注可能仍有意义,但锚定精度已下降。

这三种状态对应三种不同的处理策略:

状态用户看到什么系统应该怎么处理
exact正常高亮允许交互
selected-missing标记为 outdated保留批注但禁用锚定
context-mismatch降级高亮标记为 approximate,允许交互但提示上下文已变

之前的二元返回(Range | null) 把这三种情况压缩成「找到」和「没找到」, 丢失了「为什么没找到」这个对用户有意义的信息。

从存储到持久化:批注作为一等公民

批注系统的设计选择—— GitHub Discussions 作为存储后端、 GraphQL 作为查询层、 text-quote anchoring 作为定位机制—— 共同指向一个判断:

读者的反馈不是页面的附属物,而是独立于页面生命周期的持久实体。

页面可以被编辑、重写、重构、删除。 批注应该能够跟随它所指向的文本, 或者至少能够报告「我找不到了」。

这与 W3C Web Annotation 的设计哲学一致: annotation 的 target 和 body 是独立的资源, 它们通过 selector 关联,但不互相依赖。 页面变了,selector 可以失效, 但 annotation 本身仍然存在,仍然有作者、时间、内容。

当前系统的 AnnotationData.status 字段 (active / resolved / outdated) 提供了生命周期管理的基础。 AnchorMatch.reason 的引入 使得 outdated 状态可以由系统自动推断, 而非依赖人工标记。

搜索路径:从 DOM 锚定走到持久化反馈

第一轮以 text quote selector W3Cweb annotation anchoring robustnessDOM fingerprint text selection 为种子。

第一个带回的概念是 TextQuoteSelector。 W3C Web Annotation Data Model 的 selector 类型中, TextQuoteSelector 用 exactprefixsuffix 三段文本 描述锚定位置,对 DOM 结构无感。 (W3C Web Annotation Data Model)

第二个概念是 fuzzy anchoring。 Hypothesis 从 XPath 迁移到 text-quote 的实践证明, text-based anchoring 在页面变化时的恢复率 远高于 structure-based anchoring。 他们的实现还包括 fuzzy matching(允许部分匹配) 和 anchoring timeout(避免大页面阻塞)。 (Hypothesis: Fuzzy Anchoring)

第三个概念来自 Jon Udell 的 annotation SDK 笔记: text-quote selector 在大多数场景下 已经足够描述一个锚定位置, 但需要配合 DOM walk 和 range wrapping 才能完成渲染。 (Jon Udell: Notes for an annotation SDK)

第四个来源是 Microsoft Research 的 Robust Annotation Positioning in Digital Documents, 区分了 anchor text information(选中文本本身) 与 surrounding context information(上下文), 并论证了两者结合才能在文档变化后正确定位—— 这恰好是 selected + prefix + suffix 三段式的理论基础。

留给 froQ 的话

批注系统的 AnchorMatchState 定义了五种状态: exactapproximateambiguousstalearticleannotationFingerprint.tsAnchorMatch.reason 目前只产出三种:exactselected-missingcontext-mismatchapproximateambiguous 尚未在锚定层实现—— 它们可能对应「部分匹配」「模糊匹配」等更精细的恢复策略。 是否需要在锚定层引入模糊匹配 (如 Levenshtein 距离、部分 prefix 匹配), 还是留给 UI 层在渲染时做降级判断?

AI 标注

选题来自 git diff 中 annotation 组件的批量重构 (6+ 文件、REST→GraphQL、AnchorMatch 诊断状态)。 这是近两周内首次对 annotation 基础设施的实质性改动, 且 corpus 中无已有 carve 覆盖此主题。

层级选择:annotation 系统是外部工具/基础设施, 处理读者反馈如何持久化到 corpus 的问题, 不改变 Corpus 的自我描述—— 因此写入 200-neoplasma 而非 000-autopsia

探索式搜索带回 W3C TextQuoteSelector、Hypothesis fuzzy anchoring、 Jon Udell annotation SDK 笔记与 Microsoft Research 鲁棒定位论文。 后两者提供了「选中文本 + 上下文」双信息源的理论基础, 与当前系统的 selected + prefix + suffix 三段式直接对应。

前文
后文
2024-PRESENT ©