A corpus sweep under the doc/prose standards found 15 links whose #fragment named no anchor in its target — reworded headings, one relocated contract (tool-fs → the group README's no-timeout rule), and zh sides citing English slugs their Chinese headings never produce. Fixed all 15 (zh sides get the conventional explicit <a id> + English fragment), fixed the one generator-owned instance at its source (gen-doc-graphs), and extended verify-md-links to resolve fragments onto Markdown targets — same-file anchors included — against heading slugs and explicit <a id>, so the class is gated instead of manually grepped. Remaining probes (narrated history, duplication shingles, comment transcripts, budgets) came back clean; sibling-adapter README symmetry and implemented-note contrasts are deliberate keeps.
4.1 KiB
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(重复标题获得渲染器的 -1、-2……后缀)或显式 <a id>。指向非 Markdown 目标的 fragment(file.ts#L10)语义归渲染器所有,不在范围内;外部与根绝对 URL 同样不检查。锚点集合对任意存在的目标惰性收集,因此链入归档 note 与 vendor 文档的链接照常校验,而这些文件不会因此成为扫描源。
slug 函数是 GitHub 的算法,从 gen-cordis-catalog 的区块锚点渲染器复制而来,而非跨脚本/包边界共享;语料通过本 gate 即是两者保持同步的机制。中文侧沿用既有语料惯例(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 锚点的处方;从 TypeScript 字符串引用的锚点仍需人工 grep。
Verification
scripts/verify-md-links.spec.ts 证明各验收路径:slug 化(反引号、标点、重复后缀、显式 <a id>)、全部可解析的混合链接文档、死的同文件 fragment、死的跨文件 fragment、以及缺失目标仍报 target 而非 anchor。gate 在 doc-sync 中跑完整语料(verify-md-links,1613 个文件),且只有在 15 条修复之后才通过——语料本身就是每种衰变模式由红转绿的证据。
Alternatives considered
- 保留人工 grep 规则。 它被证明守不住:15 条 fragment 在 gate 驱动的维护文化下仍然衰变,因为改写标题的 PR 从不会去看入链。可机械检查的不变式应进入被执行的 gate。
- 让中文链接指向中文 slug 锚点。 GitHub 对 CJK 标题的 slug 没问题,但语料惯例已是显式
<a id>+ 英文 fragment(glossary、primer),且它在剥离非 ASCII 的渲染器下也存活;引入第二种惯例会割裂语料。 - 与 typert 生成器共享
githubSlug。 为一个函数引入包构建耦合不值得;算法只有三行,语料 gate 本身能探测分歧(生成器产出而 gate 无法解析的锚点,在任何页面链接它的那一刻即失败)。 - 同时校验 VitePress slug。 发布站点的死链检查已在
website:build中运行;生成区块正是为两种渲染器一致而携带显式锚点,手写标题若有分歧会在那里失败。
Consequences
重命名标题现在会在任何 Markdown 链接引用其锚点处使构建失败,而非把读者丢在页顶;作者须在同一变更中修复入链,与文件重命名的既有义务完全一致。同文件锚点不再是盲区,中文页面使用英文 fragment 时必须补锚点。人工的重命名前 grep 只对 TypeScript 字符串字面量中的锚点保留。