Files
deepseek-harness/.agents/notes/implemented/process/2026-08-09-md-fragment-anchor-gate.zh.md
T

4.8 KiB
Raw Blame History

Agent Note(agent 决策记录):verify-md-links 校验 fragment 锚点,关闭最后一类死链

Status: implemented

English | 中文

Problem

verify-md-links 只证明相对链接的目标文件存在,从不检查 #fragment,文档标准以一条人工规则补偿:重命名标题前自己 grep 锚点。一次语料扫描发现 15 条链接的 fragment 在目标中没有对应锚点——三种衰变模式:链接写下后标题被改写(#security-and-authority-are-explicit-non-goals 对 note 现在的 Security and authority are non-goals)、约定搬迁到另一份属主文档(tool-fs 链到 seam README,而无超时规则现居 group README)、zh 侧链接其中文标题永远不会生成的英文 slug(#deferred-work 对 ## 推迟工作)。这些都不触发任何 gate,且每条都把读者悄悄丢在目标页顶部。

Decision

verify-md-links 现在也解析 fragment(取代跨链接决定中被推迟的范围裁剪)。对每条目标为 Markdown 文件的相对链接——包括旧检查器完全跳过的同文件 #anchor 链接——fragment 必须命名目标中的真实锚点:标题的 GitHub slug,或真实 HTML 流中的显式 <a id>(代码示例与注释掉的锚点不注册任何东西)。slug 由仓库自有的 markdownHeadingLines 从渲染后的标题文本计算,因此标题内的链接、行内代码与强调都按 GitHub 的渲染结果 slug;下划线保留(#showcase-web_fetch);重复 slug 获得 GitHub 的占用集 -1、-2……后缀;匹配区分大小写,因为元素 id 本就区分大小写。指向非 Markdown 目标的 fragment(file.ts#L10)语义归渲染器所有,不在范围内;外部与根绝对 URL 同样不检查。锚点集合对任意存在的目标惰性收集(anchorCache),因此链入归档 note 与 vendor 文档的链接照常校验,而这些文件不会因此成为扫描源。

slug 函数与 gen-cordis-catalog 的区块锚点 slugger 不同(后者丢弃下划线):生成器的标题总能通过其显式 <a id> 锚点到达,两者无需共享一条规则。中文侧沿用既有语料惯例(docs/glossary.zh.md、docs/cordis-primer.zh.md):链接保留英文 fragment,在中文标题前放置显式 <a id>,使两个语言侧暴露相同的锚点。

15 条坏 fragment 在同一变更中修复:陈旧 slug 重定向到当前标题,搬迁的无超时约定改链其属主 group README,四份中文文档补上显式锚点。docs/AGENTS.md 与 dsh-doc-standards skill 不再为 Markdown 链接开人工 grep 锚点的处方;人工 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留(当下三处全部渲染进受检页面,gate 经由提交的产物覆盖它们)。

Verification

scripts/verify-md-links.spec.ts 证明各验收路径:渲染文本 slug 化(反引号、标点、含链接标题、保留下划线)、占用集重复后缀、围栏/行内代码/注释中的 <a id> 不注册、全部可解析的混合链接文档、死的同文件与跨文件 fragment、大小写变体 fragment、以及缺失目标仍报 target 而非 anchor。gate 在 doc-sync 中跑完整语料(verify-md-links),且只有在 15 条修复之后才通过——语料本身就是每种衰变模式由红转绿的证据。

Alternatives considered

  • 保留人工 grep 规则。 它被证明守不住:15 条 fragment 在 gate 驱动的维护文化下仍然衰变,因为改写标题的 PR 从不会去看入链。可机械检查的不变式应进入被执行的 gate。
  • 让中文链接指向中文 slug 锚点。 GitHub 对 CJK 标题的 slug 没问题,但语料惯例已是显式 <a id> + 英文 fragment(glossary、primer),且它在剥离非 ASCII 的渲染器下也存活;引入第二种惯例会割裂语料。
  • 与 typert 生成器共享 githubSlug。 为一个函数引入包构建耦合不值得,且两条规则确实不同(生成器剥离下划线;其锚点是 gate 直接读取的显式 <a id>),分歧是设计使然而非漂移。
  • 同时校验 VitePress slug。 发布站点的死链检查已在 website:build 中运行;生成区块正是为两种渲染器一致而携带显式锚点,手写标题若有分歧会在那里失败。

Consequences

重命名标题现在会在任何 Markdown 链接引用其锚点处使构建失败,而非把读者丢在页顶;作者须在同一变更中修复入链,与文件重命名的既有义务完全一致。同文件锚点不再是盲区,中文页面使用英文 fragment 时必须补锚点。人工的重命名前 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留。