关于 docs-cleaner
Docs Cleaner 是用于变更后治理和合并文档的 Skill,帮助维护者查找过时或重复内容,并在删除前评估各章节的独特价值,避免信息丢失。
主要功能
- 01变更后文档审查
- 02文档合并整理
- 03识别权威信息源
- 04查找过时事实与副本
- 05删除前评估内容价值
适用场景
- 01程序变更后检查相关文档
- 02跨文件查找过时路径或命令
- 03合并主题重叠的文档
- 04归档或删除前评估章节
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill docs-cleaner安装后在 Claude Code 中输入/docs-cleaner确认已加载。
附带文件references/delivery-entry-replay.mdreferences/value_analysis_template.md
详细介绍
Docs Cleaner
文档会反复出现各种问题,需要采用不同的方式处理。
会逐渐失效。 有些内容变了——端口、路径、流程、决策——文档却仍在描述过去的情况。业内称之为 文档腐化 或 版本漂移:资料逐一失去同步,直到人们不再信任这些文档。
会不断膨胀。 同一主题积累了三个文件,每个文件都只对了一部分,却没有一个是权威版本。
这些问题都源于一个根本原因:同一事实被记录在多个地方,而非只在一处定义。 以下所有内容都围绕解决这个问题展开。
核心原则(适用于两种模式)
先认真评估,再删除。绝不盲目删除。 在建议移除之前,先分析每个部分的独特价值。目标是在缩减内容的同时不丢失信息。
这项原则适用于此技能中所有涉及删除的操作,不仅限于内容整合:归档文档、删除派生值、移除交叉引用,都是删除,都必须先进行同样的分析。“看起来重复”不算分析。
你处于哪种模式?
| 用户的说法 / 当前情况 | 模式 |
|---|---|
| “我改了 X” / 存在差异 / 更新了流程 | 1 — 更改后治理 |
| “哪些文档现在不对了”“检查过时的命令/路径” | 1 — 更改后治理 |
| “清理一下文档”“这方面的文件太多了”“把这些合并起来” | 2 — 内容整合 |
| 某个主题分散在多个文件中,且没有明确的负责人 | 2 — 内容整合 |
两种模式都会使用下方的漂移测试。两者都适用时,先运行模式 1:整合同样存在事实错误的文档,只会产出一份看起来很有把握、实际却错误的文档。
**关于 CLAUDE.md / AGENTS.md 的说明:**两种模式都适用于这两类文件,就像适用于其他任何文档一样——更改后进行治理,彼此内容重复时进行整合。唯一应交由其他工具处理的任务是重构单个超大型文件——将低频细节移入参考文档,同时保持顶层内容精简。这正是 daymade-claude-code 的 claude-md-progressive-disclosurer 技能的用途;如果问题在于篇幅和层次,而非事实准确性或内容重复,优先使用它。如果该技能不可用,模式 2 仍可处理该文件——只是需要手动进行价值分析。
漂移测试(两种模式的核心)
在将任何值写入文档之前,以及判断现有值是否有保留的必要时,都要按顺序提出以下问题,并在第一个命中时停止:
1. 能否根据此处或权威来源中已记录的细节计算出该值? → 这是一个派生值。不要写入。有人询问时再计算。 示例:计数(“6 个章节”“涵盖 N 个组”)、总计、概述下方表格中各行的状态,或日志下方已有记录时的“最后更新日期”。
删除某个值时,要修正句子,而不是留下空缺:“以下四项检查”应改为“以下检查”。删除数字后留下语法残缺,是另一个缺陷,并非修复。
例外——自动生成的导航。 按此测试,重复列出各级标题的目录属于派生值,手动维护的目录确实应当删除。但如果目录由文档工具链在构建时生成,或平台要求使用标记才能呈现导航,则应保留:这些内容是按需计算的,正符合问题 1 的要求。删除前先检查是否有生成器。
2. 无法计算,但同一事实已在其他地方权威定义? → 这是一个副本。链接到该定义。不要重述该值。
3. 既无法推导,也不是复制的内容——而是独立记录了某个特定时刻的真实情况? → 可以将其保留为原子级历史观察:带日期的变更、原始测量值、价格观察或决策。绝不要用当前值或当前配置的链接替换过去的值。
对每项陈述应用此顺序测试,包括变更日志、事后分析和带日期的笔记中的陈述。日期并不能让本地可推导的总计、比率或复制的摘要免于问题 1 和问题 2 的检验。删除此类派生或复制的重述,同时保留独立事件、测量值和因果背景。
例如,带日期的上传限制变更记录的是当时的变更;今天的配置则是另一项事实。区域新增事件在删除可根据其记录的历史区域详情计算出的总数后,仍然成立。不要用今天的区域重新计算旧总数。
通过问题 1 和问题 2 检验的独特历史原子事实,并不属于漂移修复。其内容价值仍须根据现有授权接受模式 2 的分析。当证据无法确定某项陈述是可推导的、复制的还是独立观察到的,应将其保留为未解决事项并报告这一缺口;不要臆测删除依据。只有当预期操作超出现有授权时,才需要新的批准。
链接只能解决情况 2。 将链接指向派生值属于分类错误:没有一个唯一的权威单元格可供链接,因此该链接会变成自身也会过时的脚手架,还会造成两处内容一致的假象。
为什么顺序很重要: API 文档应对漂移的成熟做法是“从源生成,而不要手动复制”(OpenAPI/Swagger 的存在正是出于这个原因)。问题 1 将同样的思路应用于文本——确保数字正确的最可靠方法,就是不要将它写下两次。
位置不构成豁免
事实无论位于何处,都是事实。对于上述测试,以下内容都属于正文,而过时的值在审计中正是会藏匿于这些位置:
frontmatter · YAML/JSON 元数据 · 括号内补充说明 · 索引表中的描述列 · 复选框状态 · 正文内容开始前的引言框架 · 跨文档脚注 · 添加到表格的额外列 · 留在正文中、形似提交消息的笔记
什么算事实
任何会随时间变化、且在某处有权威定义的内容:数字、状态、所有权、关系、分类、规则、决策来源、日期、计数。业务规则也算——“所有报价都由财务负责人处理”只定义一次,后续文档引用即可,无须逐份重述。
模式 1 — 变更后治理
按以下顺序执行。顺序至关重要:尚未确定权威来源就先修复派生文档,只会成倍增加工作量。
1. 根据变更界定范围,而不是根据仓库界定范围。 列出这次变更改动的事实——这个端口、这条路径、这个流程名称、这个默认值。此列表就是工作的边界。列表有边界;对其中每一项的搜索没有边界——事实不易察觉的副本恰恰存在于这次变更未触及的文件中,因此第 5 步要在整个仓库中搜索每一项。禁止的是另一种做法:打开文件寻找无关问题。无边界的全面检查会让文档任务演变成无法审查的重构。
2. 为每项受影响的事实确定权威来源(SSOT)。 哪个文件定义了这个端口/路径/流程,而不只是提及它?先更新那个文件。所有下游内容都应引用它,或由它派生。
当流程从 Skill 条目移入参考文档时,要将使用该流程的 CLAUDE.md / AGENTS.md 指针纳入此范围。使用
delivery-entry replay 检查旧导航和修复后的导航是否都指向实际目标;仅仅可以到达目标,并不能证明目标仍定义了所需流程。
判断“定义”的实际标准是:要改变现实,必须修改哪个文件——部署实际读取的文件、代码加载的架构,或人们逐步遵循的操作手册。某个文件在解释其他内容时顺带复述某个值,这属于提及。当两个文件看起来都像声明性定义时,按以下优先顺序选择:机器会读取的文件优先于只有人会阅读的文件;离描述对象更近的文件优先;其他文档已经引用的文件优先。然后在报告中说明你选择了哪个文件,以及依据上述哪条标准——第 2 步一旦将其他所有内容指向该文件,任意选择就会固定下来,因此应将其作为明确决定说明,而不是由你先打开哪个文件偶然决定。
3. 根据既定意图和授权解决实现分歧。 将代码、脚本、配置和实际运行情况视为当前行为的证据,而非自动视为预期行为的权威来源。如果需求、权威来源或测试明确了预期行为,并且用户已授权在限定范围内修复实现,则先修复并验证实现,再更新文档使其一致。修复范围应限于任务已授权的事实和文件。
如果预期行为不明确,或实现变更尚未获得授权,则报告分歧,并在编辑代码前取得缺失的决定或授权。不要让文档描述期望中的行为,也不要悄悄把文档更新变成实现项目。
4. 编辑前,先决定每份受影响文档的处理方式。
| 文档属于…… | 操作 |
|---|---|
| 仍有实际用途;只有具体事实已偏离 | 更新 |
| 建立在已不成立的前提之上 | 归档/停用 |
不要修补前提已不成立的文档——这样只会得到内部一致、全局错误的内容,比一眼就能看出过时的文件更难发现。
行内保留“本节已被取代”的说明并非第三种选择。维护者的记录工作不应出现在读者的阅读路径中;读者缺少理解其含义的背景,只会因此困惑。应将其移出。
“归档”是指:移出读者的阅读路径,但保留在历史中。 具体来说:查找项目已有的约定(_archive/、archive/、deprecated/ 或项目使用的其他目录),并遵循该约定;如果没有约定,就在该文件所在层级创建 _archive/。无论哪种情况,都使用 git mv 移动文件,以保留历史记录,然后重新指向或删除所有指向它的引用。不要直接删除,也不要只添加 status: deprecated 前置元数据,却仍把文件留在读者能找到的地方——一份仍在读者阅读路径上的退役文档,仍然会被阅读。
归档任何不是你创建的内容前,先征求同意,原因与阶段 4 的步骤 3 需要达成一致相同。无人值守运行且无法询问时:不要改动文档,并报告该文档需要退役。 不要改写它来代替归档——一份前提已失效的文档,更新其事实只会让它更具误导性。
5. 在修改之前,找出每个已更改事实的所有副本。 这个步骤最容易被跳过,也是典型问题出现的地方:显眼的那处更新了,另外三处不起眼的却没有。确切命令见下文的验证部分。
风险最高的情况是在序列中重新编号或插入项目,因为你改动的不是一个事实,而是一组相互引用的事实:正文中写着“参见步骤 7”的内容、同级文件中的交叉引用,以及——最棘手的——把数量写进句子的文字(“下面的四项检查”)。最后一种情况不包含你要搜索的任何数字,因此按步骤编号搜索永远找不到它。
对此没有机械式的全面检查方法,不要假装有。 两种显而易见的搜索范围都经过验证,且都行不通:
- 在整个代码库中搜索数量词 → 在真实文档代码库中会匹配 224,330 行 / 约 550 MB(限定到单个项目目录时为 17.7 MB)。没人会运行这种搜索,而一条注定会被绕过的指示比没有指示更糟。
- 只在提及该主题的文件中搜索数量词 → 范围有限,但在关键方面是错的。把数量写进句子的文字,恰恰会用别名、译名、代词或裸链接来指代主题,而不会使用标题——“下面的四项检查”、“「预检有四个检查」”。实测结果:一个确实包含过时数量的文件,对主题名称或文件名的搜索结果为 零。这个筛选条件与目标呈负相关。
因此改用以下方法,并报告其未覆盖的内容:
-
通读重新编号的文件。 大多数过时的自我引用都在其中。
-
先构建文件列表,再在其中搜索数量词。 Mode 1 中此前没有任何步骤会生成这个列表——不要使用更新引用中的入站引用搜索;那属于 Mode 2,回答的是另一个问题(“哪些内容指向这个文件”,而不是“哪些内容讨论这个主题”)。在这里生成列表:
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not in a git repo — pass the docs root explicitly" >&2; exit 1; } rg -l -F "<the subject's name>" "$ROOT" --no-ignore --hidden -g '!.git'这样可以找出明确写出主题名称的文件;步骤 3 会覆盖没有写出主题名称的文件。列表很短,因此直接列出文件名,不要把列表通过管道传给
xargs(列表为空时,GNUxargs会在没有路径参数的情况下运行搜索,转而搜索当前目录——范围错误,却看起来像是有输出):rg -n -i -H \ -e '\b(one|two|three|four|five|six|seven|eight|nine|ten|both) [a-z]+\b' \ -e '[一二三四五六七八九十两](个|条|步|项|点)' -e '[0-9]+(个|条|步|项|点)' \ -e '\b[0-9]+ [a-z]+\b' \ path/to/one.md path/to/two.md有两个细节都经过了实测:使用
-H,因为rg在只给出一个文件时会省略文件名;而这里需要的全部答案就是“哪个文件”。此外,模式会有意匹配后续的任意词语,而不是一份名词列表——列表中不会包含项目实际使用的名词(“四个关卡”、“三条通道”),漏掉时也不会有提示。对少量文件来说,多出的噪声不碍事;漏掉的那一行却很要紧。 -
自行列出主题的别名并搜索它们,然后在搜索结果中查找数量词。这一步没有任何工具能替你完成。 然后说明你检查了什么:“已阅读重新编号后的文件;已在按名称引用它的 N 个文件中搜索计数词;未通过机械方式检查以其他名称提及它的文本。” 这句话比一个悄无声息地表示上述两种失败之一的绿色勾选标记更有价值。
6. 清理沿途遇到的派生值——但仅限于你已经在编辑的文件。 删除你当前打开的文件中的过时计数属于维护工作,不是扩大范围。打开无关文件来寻找更多问题,就是扩大范围。明确这条界限,才能让改动易于审查。
7. 按照 需要报告的内容 中的结构报告你做了什么。
模式 2 — 整合
在保留所有有价值内容的同时整合冗余文档。上述 核心原则 约束此模式中的每一项删除。
阶段 1:发现
- 找出涵盖该主题的所有文档文件,并在搜索前说明你搜索的范围(一个目录、一套文档、一个项目)——与模式 1 第 1 步采用相同的范围限定规则。给两个 Agent 下达“文档”这一指令却不指定范围,它们整合出的文件集合会不同。通常的做法是在该范围内搜索主题关键词——注意使用
-F,因为C++、.NET或v1.0.0等主题词都是字面量,将它们作为模式时几乎能匹配所有内容:rg -l -i -F -e '<topic term>' -e '<synonym>' <boundary> --no-ignore --hidden -g '!.git'这份列表限定了整个整合工作的范围,因此列表质量差,分析结果也会很差。 - 统计所有文件的总行数——对第 1 步的文件列表运行
wc -l - 梳理内容重叠情况:针对每个文件中的每个标题,记录其他哪些文件也涵盖了相同内容。最终产物是一张“标题 → 也涵盖该内容的文件”表;在多个文件中出现的章节就是整合候选项。此步骤只用于判定重叠情况——不决定保留还是删除。独有章节仍可能在第 2 阶段因与重复无关的原因(显而易见、已被取代、属于副产品)而被删掉,因此应将其理解为*“这里没有任何依据可以把独有章节当作冗余内容删除”,而不是“独有章节一定会保留”*。
第 2 阶段:价值分析
对每份文档逐节建立表格:
| 章节 | 行数 | 价值 | 原因 |
|---|---|---|---|
| API 参考 | 25 | 保留 | 独有的端点文档 |
| 设置步骤 | 40 | 精简 | 冗长但不可或缺 |
| 环境变量表 | 30 | 删除 | 与 README.md 中的表格重复,后者是权威来源 |
删除示例指出了权威的重复内容。历史主张也要按相同顺序接受 Drift Test;历史事件与当前配置不一定是同一事实。
-
保留 — 独有、必要且经常被引用
-
精简 — 有价值但冗长
-
删除 — 重复、一次性、不言自明或已过时
-
归档 — 整份文档的前提已失效(已取消系统的设计说明、已退役服务的事后分析)。这不是需要更正的当前状态过时信息,也不是需要删除的重复内容。没有此类别时,“已被取代”和“这是记录”会将同一份文档拉向相反方向,最终由最后读到的说法决定。
活跃文档中前提失效的章节,在更低一层也会遇到同样的问题,而归档不适用——它的定义范围是整份文档。应将其视为删除,并将原因写为前提已失效,在第 3 阶段计划中注明,而不是自行决定:“Kafka 迁移章节,所述迁移已取消”,正是所有者可能会回应“其实我们仍会引用它”的情况。
归档判定会将该文档移出本次整合范围。 不再进入第 3 阶段或第 4 阶段——没有内容需要合并或精简。按照模式 1 的归档流程处理——模式 1 — 变更后治理中以
“归档”指的是:脱离读者的阅读路径,但仍保留在历史记录中开头的段落——在报告中说明已执行归档,然后继续整合其余内容。也不要将它纳入第 3 阶段的行数计划:它会原样移出,若将其行数计为“减少”,就等于声称实现了并未实现的节省。
逐行分析时,对每一行都执行 Drift Test,并让测试结果在两个方向上覆盖类别判定:
- 仅重述可从其他来源计算出的值的章节,无论写得多好,都应判为删除(问题 1)。
- 历史章节中的每项主张仍须分别接受问题 1 和问题 2 的检验。可在本地推导出的汇总数据和复制的重述内容可以删除;独立的原子观察结果可在问题 3 下保留。在价值分析中注明拟删除的任何独有历史原子信息,并遵守现有授权;不能仅以年代久远或“仅此一次的记录”为由删除。
详细标准:references/value_analysis_template.md。
第 3 阶段:整合计划
提出包含具体数字的目标结构,并提交协商确认——第 4 阶段要求你在删除任何内容之前先获得批准:
Before: 726 lines (3 files, high redundancy)
After: ~100 lines (1 file + a pointer from the project's main doc)
Value preserved: pending — established by the preservation check after execution
前后行数衡量的是拟议结构。“保留的价值”不是:在计划阶段,这相当于由负责删除的一方自行断言删除无害,正是这项技能旨在消除的自我认证模式。
这项内容要在第 4 阶段第 7 步之后填写,而且不是百分比——百分比的分母必须凭空编造。它是针对明确分母给出的数量:“第 1 步清单中的全部 34 项均保留下来”,或 “34 项中有 31 项”,并指出缺失项及各自的去向。
分母仅包含保留和精简项。 第 1 步的清单刻意也涵盖了删除章节,而这些内容本就应该从整合后的文件中消失——把它们计入这里,会导致“全部保留”按定义不可能实现,而没人能达到的目标,人们也就不会再努力追求。它们应单独列出:“此外,删除章节中的 6 项内容,逐一说明其副本保留在何处,或删除的原因”。两个数字,两个问题,彼此都不掩盖。写出分母,而不只是结论——“我检查过的每一项都保留了”,无论是 1 项还是 34 项都可能成立,因此它反映的是你的审慎程度,而不是文档的最终状况。只要不是全部保留,就是需要报告的发现,绝不能把数字向上取整,也绝不能为了让报告更好看而撤销自己的精简。
第 4 阶段:执行
即使 git 可以撤销,删除他人文档对读者而言仍是不可逆的。请先提交第 3 阶段的计划并获得同意,再执行第 3 步。 第 3 阶段要求“提出计划”是有原因的;核心原则约束的是分析的严谨程度,而不是你是否获得了授权。如果你在无人值守的情况下运行,无法征求同意,就执行第 1–2 步,然后停下来报告你打算删除的内容。
-
先提取保留证据,再开始编写整合文档的任何内容。 顺序决定了检查是否有意义:如果在改写之后才写声明清单,它列出的就只是你恰好保留的内容,因此总会与结果一致。提前写下,它才是改写必须满足的规范。
如实说明它的约束力:最终产物中没有任何内容能区分清单是提前写下的,还是事后整理出来的,因此这只是约定,并非门槛。请尽可能赋予它约定所能有的约束力——在开始第 2 步之前,先将清单写出,放入报告或临时文件。改写之前就以书面形式存在的清单可以用于核对;事后才出现的清单则无法与结果的转录区分开来。
单位是每个承载关键信息的事项一条,而不是每个章节一条,也不是每个类别一条。 如何细分取决于判断,两位审慎的 Agent 得出的数量不同也可以接受。不可接受的是粗略拆分时遗漏的条款,因此无论采用何种计数方式,以下两项都必须各自单列:
- 后果条款。“重试前先回滚”是一条指令;“在只应用了一半的迁移上重试会损坏
events表,而且没有任何机制能自动修复”才是人们遵守这条指令的原因。粗略的清单会保留指令,丢掉后果,而后者正是让信息经得起快速浏览的关键。 - 症状 → 原因映射。“部署步骤返回 401,意味着令牌已过期,而不是账户有问题”无法从“令牌每 90 天过期一次”中重新推导出来。这是故障排查内容,而且只有一行,因此最先消失。
这两项是底线,不是判断重要性的边界。 之所以特别提到它们,是因为粗略列表最容易漏掉这些条款——而不是因为它们以外的条款就安全。还有三类容易漏掉、值得手动检查的内容:顺序约束(“先应用迁移,再重启工作进程”——没有说明后果,因此第一条规则不适用)、范围限定(“这仅适用于
staging集群”),以及隐藏在正文中的阈值(“最多重试 5 次”)。每一项都只有一个条款那么长,每一项都会改变读者的做法,而且都不会主动显眼地标出来。
逐一检查每个章节——包括标记为 Delete 的章节——并根据源内容写下:
- Keep → 其中每个条目各摘录一行有辨识度的原文(每个注意事项、每个约束、每个代码片段都要列出——包含三个注意事项的章节就应有三行,而不是一行)
- Condense → 每一项承载关键信息的主张:所有约束、数字、条件和警告
- Delete-as-duplicate → 列出其中的条目,并注明每项保留副本的位置。 这一步最容易被跳过,却能抓出最严重的遗漏:Phase 1 按标题映射重叠内容,因此某个章节可能因为标题相同而被标记为重复,但其中却有一个其他文件都没有的注意事项。跳过清点,这个注意事项就会在之后的每项检查中隐形——它从未列入清单,所以第 7 步会报告“每项内容都已保留”,而这竟然是真的。
- 因其他原因删除 → 列出其中的条目,并说明每项为何删除。说得出理由的删除才是决定;说不出理由的删除,只是尚未察觉的意外。
- 上述所有内容 → 列出每个 URL 原文
这份清单能做什么,不能做什么。 它能发现回忆方面的遗漏——你本打算保留某项内容,却没有保留。它无法发现判断方面的失误:如果你在 Phase 2 判断某项内容无关紧要,它就不会进入清单,而第 7 步也会轻松通过,不会发现遗漏。这个缺口确实存在,本流程中的任何步骤都无法弥补。 Phase 3 计划会交给人工审核,但其中只有目标结构和行数——没有这份清单——因此也无法发现误判的条目;它检查的是结果的形态,而不是纳入了哪些内容。不要把第 7 步通过报告成没有丢失任何有价值内容的证据——应如实说明,它只确认你识别出来的内容都已保留。如果你还想审核判断是否得当,唯一的办法就是把条目清单交给一个没有参与编写的人查看。
继续之前先做一项合理性检查:条目数量看起来符合源材料的规模吗?一份三页的操作手册最后只列出四项,说明你只是粗略浏览了——而单薄的清单恰恰最容易在之后通过检查。
- 后果条款。“重试前先回滚”是一条指令;“在只应用了一半的迁移上重试会损坏
-
创建整合后的文档,保留清单上的每一项
-
删除冗余的源文件
-
更新所有对它们的引用(
CLAUDE.md/项目主文档、README、导入语句) -
在主文档或 README 中留下指向新位置的链接——如果原本没有,就创建一个。整合会把内容移到目前还没人知道如何找到的位置;若不执行这一步,只有执行整合的人才能找到这些内容。
-
确认没有失效链接(见下文更新引用——仅搜索裸文件名并不足够)
-
宣布完成之前,使用下方的保留检查进行验证,并使用第 1 步中的清单。这是本阶段的关卡,不是可选附录。 如果你跳过了步骤 1,且源文件曾由 git 跟踪,那么仍然可以恢复它们 — 但要注意使用哪个 ref:删除尚未提交前,内容位于
git show HEAD:<path>;删除提交后,该路径便不再存在于 HEAD 中, 需要使用git show HEAD~1:<path>(或删除前的提交)。路径相对于仓库根目录, 因此在子目录中请使用git show HEAD:./<path>。两种错误形式都会明确报错fatal: path ... does not exist,所以你会知道。绝不能从汇总文件中提取预期 字符串 — 那样是在拿它和自身比较。
验证
让文档检查做到字面匹配(事实可能包含正则表达式引擎会视为通配符的字符 —
按模式搜索时,v1.0.0 也会匹配 v1X0X0,而 [2026-08-13] 会因字符范围无效而中止)
以及连续匹配(跨两行的事实是一个事实,而不是两个)。
ripgrep 两者兼具:-F 按字面匹配,-U 允许匹配跨行。
交付字面命令示例之前,应在隔离的测试夹具中,以其声明的 Shell 和环境运行原样的安全代码片段。将其接受的标志、标志优先级、结果和退出状态与实现或 CLI 合约进行比较。不要针对在线目标执行具有破坏性、会向外发送内容、会修改生产环境或包含凭据的示例;应通过代码或帮助输出核实其标志和优先级,并说明哪些端到端行为未经测试。
始终传入明确的路径。 这不是风格偏好:未提供路径参数且标准输入为非交互模式时 — Agent 的 Shell 就是如此 — rg 会搜索标准输入,而不是你的文件。具体出现哪种症状取决于标准输入,两种情况都会造成误判:
- 标准输入已关闭或管道已耗尽 → 退出码为 1,无输出。与“字符串在任何地方都不存在”完全相同,因此这个错误看起来像是检查顺利通过。在一个有 4 个文件包含该字符串的目录中,不带路径的写法什么也没报告。
- 继承的标准输入永远不会到达 EOF → 命令会挂起,直到有进程将其终止。如果搜索莫名挂起,请先检查是否遗漏了路径参数,再断定文件系统运行缓慢。
每次遍历目录的搜索都必须带上两个标志,原因详见是什么让这项检查可信:rg 默认会跳过 VCS 忽略的文件和隐藏文件,而 .github/ — 其中有许多有文档记录的命令 — 是隐藏目录。没有这些标志的搜索只会检查部分文档(实测:4 个文件,而不是 6 个)。明确指定文件的搜索则不需要这些标志 — 无论忽略规则或隐藏规则如何,rg 都会读取你传入的路径 — 因此本文件后面的逐项检查会省略它们。
设置一次搜索根目录,并确保出错时明确失败,而不是悄悄缩小范围:
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not in a git repo — set ROOT to the docs root by hand" >&2; exit 1; }
这里不要写 || echo .。静默回退到 . 会将搜索范围限定到你恰好所在的目录,并且仍然以 0 退出 — 因此“整个仓库”会悄悄变成“当前子树”,而位于上一级目录的事实会被报告为不存在(实测如此)。如果你不在仓库中,请手动将 ROOT 设为文档根目录;关键是这个路径是你主动选择的。
# Every copy of a fact, before you change it. Path and both flags are load-bearing.
rg -U -F "the exact fact string" "$ROOT" --no-ignore --hidden -g '!.git'
# Per-file counts — the artifact you paste into your report.
rg -U -F --count-matches "the exact fact string" "$ROOT" --no-ignore --hidden -g '!.git'
# Multi-line facts: the newline has to be a real newline. In bash/zsh that means $'...';
# "\n" inside double quotes is a literal backslash-n and matches nothing (measured).
rg -U -F $'first line of the fact\nsecond line of it' "$ROOT" --no-ignore --hidden -g '!.git'
启用 --no-ignore 后,-g '!.git' 不可省略:没有它,搜索会报告 .git/COMMIT_EDITMSG 和 .git/logs/HEAD 中仍有匹配内容 — 也就是描述你所做更改的提交消息 — 仿佛还有未清理的副本。这些内容永远无法“清理”,因此,依赖这类计数的检查会让你习惯于无视计数不符,从而使其无法发现真正的问题。--no-ignore 还会重新纳入 node_modules/ 和其他供应商目录;出现这些目录时,也要用同样的方式将它们排除。
搜索前先记下预期计数,而不是搜索后再写。 看过输出后才决定的计数永远不会出错。先说明哪些文件中应该仍包含该值、哪些文件中不应该包含,然后运行:
rg -U -F --count-matches "the old value" "$ROOT" --no-ignore --hidden -g '!.git'
rg -U -F --count-matches "the new value" "$ROOT" --no-ignore --hidden -g '!.git'
将每项结果与清单进行集合相等性比较,检查两个方向:出现了但不该出现的文件是残留文件;清单中有但结果里没有的文件,同样意味着失败——--count-matches 会完全省略匹配数为零的文件,因此,本该获得新值却悄无声息地未更新的文档,看起来和一次干净的运行毫无区别。
如何确保这项检查可靠:
- 旧值不一定总会归零。 变更日志、决策日志,或“截至 ,该值为 X”之类的句子记录了历史,会合理地保留旧值(Drift Test 问题 3)。为了让计数归零而删除这些内容,会破坏审计记录。提前列出历史文件;清单之外的任何文件都是残留文件。
- 特意使用了
--no-ignore --hidden,而且这两个标记并不相同。 默认情况下,rg会同时跳过被 VCS 忽略的文件和隐藏文件。--no-ignore只会恢复前一类;不加--hidden,搜索仍然不会打开.github/,而贡献指南、议题模板以及包含大量命令说明的工作流都在这里(实测:添加--hidden后,样例搜索的结果从 4 个文件增至 6 个)。看不到这些目录的收敛检查,即使仍有过时副本,也会显示通过。请将它们纳入搜索,再判断多出来的匹配是否重要,而不要想当然地认为它们无关紧要。
信任零结果之前,先校准搜索——无论使用哪种工具。 搜索无结果有两种原因:事实已经不存在,或者命令有误。两者看起来完全一样。在报告“没有剩余副本”之前,先用同样形式的命令搜索一个你确定存在的字符串。如果它也没有返回结果,出问题的是搜索工具,而不是文档。这是对前文无文件路径 rg 故障的直接补救,而且只需多写一行。
不使用 ripgrep
git grep -nF "fact" -- . 适用于任何 git 仓库,并且是跨平台的一种实现(不同于 grep,其递归和符号链接行为在 BSD、GNU 及替代实现之间各不相同)。它有以下局限,均经过实测,而且都会给出看似可信的错误答案,而非报错:
它只搜索已跟踪文件。 你刚在模式 2 中编写的合并文档是新文件,尚未跟踪——因此,用
git grep进行保留检查时会报告其中的内容缺失,让你去“恢复”根本没有丢失的内容。请先对它执行git add,或者直接读取该文件。
-F遇到多行字符串时,会悄悄退化为分别匹配各行。 如果文件只保留了两行事实中的第一行,仍会报告匹配成功——这样,有损迁移就会被认证为“内容仍然存在”,而这恰恰是保留检查要抓出的错误。grep和git grep都有这个问题。
如果这两种工具都不可用,可以在任何能运行 Python 的环境中执行连续内容检查:
python3 -c 'import sys, pathlib
# .rstrip("\n") matters: your editor adds a trailing newline to needle.txt, and a fact
# sitting at a file'"'"'s end has none — without the strip, that copy is silently missed.
needle = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8").rstrip("\n")
for f in sys.argv[2:]:
n = pathlib.Path(f).read_text(encoding="utf-8").count(needle)
if n: print(f, n)' needle.txt doc1.md doc2.md
更新对已移动或删除文件的引用
对于拆分条目或重命名章节,交付条目回放提供了有界的过期锚点控制和修复后条目检查。对照实际标题阅读正文中的裸章节引用;链接检查器通过并不能验证它从未解析为链接的正文内容。
搜索裸文件名只能找到部分入站链接,遗漏其余链接,然后还会报告“已验证”。文件引用的形式不止一种:
BASE=old-doc # filename without extension
rg -n -F -e "$BASE.md" -e "[[$BASE" -e "($BASE" -e "/$BASE" "$ROOT" --no-ignore --hidden -g '!.git'
这涵盖了 Markdown 链接、维基式 [[old-doc]]、裸路径和导入。它不会替你完成以下事项:
-
文件更改目录深度时,相对路径会失效。 在一个层级下的
../guide.md,到了另一个层级可能变成../../guide.md。grep 文件名能找到该行;但路径是否仍能解析,是另一个问题,需要逐条链接检查。 -
“验证没有失效链接”包含两个方向,而 Phase 4 只要求你完成其中一个。 入站——“是否仍有内容指向我删除的内容?”——这是合并整理可能造成的问题,上面的搜索可以回答。完成这项检查;这是该步骤的实际要求。
出站——“这些文件中的每个链接是否仍能解析?”——这是一般的文档 QA 工作,不应在这里临时编写脚本。请使用真正的链接检查器:
lychee --offline --root-dir "$PWD" docs/ # local paths only, no network lychee docs/ # also checks external URLs (slow, fails for unrelated reasons)如果文档使用根相对链接(
/docs/x.md——mkdocs、Docusaurus、GitHub Pages 下的常见样式),--root-dir就不可省略:没有它,lychee 会将所有此类链接报告为错误,这正是下文所说应避免的误报。除非添加--include-fragments,否则不会检查锚点(x.md#section)。0 Total不代表通过——在相信它之前先校准。 实测结果(lychee 0.24.2):不存在的路径会明确报错并以退出码 1 结束;但一个存在且不含 Markdown 的目录会输出🔍 0 Total … ✅ 0 OK 🚫 0 Errors,并以退出码 0 结束——与干净运行完全相同。路径指到高一层或深一层的目录,就会得到一个看似正常、实际什么也没检查的绿色勾号。查看Total数值:如果为 0,要么确认文件确实不含链接,要么说明你查错了位置。(链接全部位于行内代码或围栏代码块中的文件,确实会得到 0;lychee 会正确跳过这两种内容。)lychee(brew install lychee/cargo install lychee)能够处理 shell 管道无法处理的 Markdown 链接形式:尖括号链接[x](<my file.md>)、带标题的链接[x](y.md "Title")、嵌套括号、引用式链接[x][ref],以及围栏代码块中的链接。这些形式都会让基于正则表达式和test -e的方法失效,在入站和出站检查中都会静默漏掉真正的失效链接,并错误标记有效链接。如果没有可用的链接检查器,请说明未运行出站检查,而不是用手写脚本替代。检查器只会验证它解析到的链接和片段。它对标题、锚点和自动链接的处理规则可能与目标读者所在平台不同。例如,包含变体选择符(VS16)的表情符号标题,或紧邻标点的裸邮箱地址,可能导致检查器报错,即使平台上的永久链接或
mailto:目标能够正常工作。这些是解析器兼容性示例,不是可移植的标题 slug 算法。在判定链接失效或修改链接之前,请在目标读者所在平台检查确切的 Markdown 内容:打开渲染后的标题或永久链接,或检查邮箱实际的
mailto:目标。确认目标缺失或不匹配后,才归类为真正的失效链接;如果平台上的目标有效但检查器报错,则归类为解析器兼容性问题。如果无法检查该平台,请将读者侧解析情况标记为未验证,而不要根据检查器结果推断。进行兼容性修复时,请保留可见标签和联系人身份,然后同时验证检查器结果和读者实际看到的目标。不要仅为消除诊断而删除有用条目或更改预期目标。
保留检查(模式 2)
这项检查的旧形式是一份待勾选事项清单——“保留了必要流程”“保留了注意事项”。删除内容的人自己勾选,因此检查永远不会失败。请用证据取代自我评估:逐一列出具体事项,并指出它现在位于何处。下文的类别是用来检查的类型清单,而不是统计证据的单位。
使用你在第 4 阶段第 1 步中整理的清单——每个承重事项占一条,且须在改写前写好。(错过了这一步?第 4 阶段第 7 步说明了如何从 git 恢复来源,以及应使用哪个 ref。)绝不能从合并后的文件中提取预期字符串:那只能证明文件与自身一致。
验证方法取决于处置方式,用错方法会生成错误的删除报告。 标记为 Keep 的章节应逐字保留;标记为 Condense 的章节本来就应该改写,因此搜索旧句子的原文理应找不到——这说明压缩方式奏效了,并非内容丢失。
| 处置方式 | 如何证明内容得以保留 |
|---|---|
| Keep,原样保留 | 每条已保存事项:rg -U -F "<saved distinctive line>" <consolidated file> → 找到匹配项 |
| Keep,与另一章节合并——仅限在改写前记录了合并计划的情况(在第 3 阶段计划或第 1 步清单中) | 逐字搜索会找不到匹配项——将用不同措辞表达相同约束的两个章节合并后,会产生第三种表述,而这正是模式 2 的目的。使用下文的 Condense 方法。不要把原句粘贴回来以让搜索显示绿色通过:这会重新引入你正在消除的重复内容。 |
上述两行适用哪一行必须在改写前确定,绝不能在检查时决定。 如果你无法指出计划或清单中说明该章节要合并的条目,那么就适用上一行,搜索不到就是没找到。这不是文书工作:让刚刚搜索失败的 Agent 自行选择评分标准,会把严格的逐字检查变成宽松检查,而且只会在它刚发现问题时放宽标准。这样放过的并非合并,而是改写时悄悄重写的普通 Keep 章节,其后果条款被删掉,再被重新标记为“已合并”,好让宽松的方法在弱化后的句子上找到匹配项。 | Condense | 对每条已保存主张(每项约束、数字、条件、警告):引用合并文件中承载该内容的行。无法指出对应行的主张就是被删掉了,无论正文有多长。 | | Delete | 对每条已保存事项:指出保留下来的副本在哪里;如果没有副本,则说明删除理由。“看起来多余”不是理由;指出其中原本包含的内容才是。 |
下方的类别是回忆提示,而非统计单位——它们用于提醒你注意可能遗漏的类别,而每项仍须按照上表提供逐项证据。勾选类别不算逐项证据。
| 类别(Keep 和 Condense 均适用) | 应提供的证据 |
|---|---|
| 必要流程(设置、配置) | 按上方对应处置方式的要求 |
| 关键约束和注意事项 | 同上,每项注意事项各提供一条证据——这些承重主张最容易在压缩时丢失 |
| 故障排除指南 | 同上 |
| 技术债务 / 路线图事项 | 同上 |
| 外部链接和参考资料 | 从 Keep 或 Condense 章节保存的每个 URL,都应在新文件中原样出现(检查字符串是否存在,不发送 HTTP 请求)。压缩时 URL 应完整保留,否则就是被删掉了——不存在“更短的 URL”。从 Delete 章节保存的 URL 则按 Delete 行处理:说明链接在哪里保留,或者说明将删除它以及原因。不要把 URL 粘贴回合并后的文件以让这一项显示为绿色通过——这会重新引入你决定删除的内容 |
| 调试技巧和代码片段 | 同上;即使位于压缩后的章节中,代码也必须逐字保留 |
| 命中只能证明字符串在文件中,不能证明它位于正确的位置。 一条仅适用于暂存环境的警告,若落在“Production”标题下;或一项约束若移出了限定它的条件之下,都会通过这项检查,却会传达错误信息。如果某项内容的含义取决于它所在的位置,就要阅读周围的行,而不是找到匹配项就停下——这也是为什么有损的多行移动仍可能被认定为“内容还在”。 |
任何无法找到命中的内容都没有得到保留——它已被删除。这可能是正确的做法,但应在报告中列为删除,而不是打勾。
删除数为零也是合理的结果。 如果价值分析表明每个章节都值得保留,就报告“无需整合”并结束。减少行数绝不是目标——如果 Agent 为了有数字可报而删除内容,那就完全背离了初衷。
报告内容
明确说明以下各项。对任何一项保持沉默,都会让人觉得“无话可说”,而这种情况很少属实:
- 发现的不一致——哪些内容彼此矛盾
- 采用的权威来源——你将哪个文件视为每项事实的定义来源;如果有多个文件看起来都在定义事实,说明你基于什么理由作出选择
- 修改的文件及原因——每个文件用一行说明
- 删除的派生值和冗余引用——说明它们分别位于何处,以及为什么它们属于派生值,而非值得保留的事实
- 搜索证据——运行过的命令及其计数,以便核查上述每项声明,而非只能听信。相同的材料也列在输出产物中;两处清单指的是同一份报告,而非两份。
- 修正后的完整流程——如果流程有变化,端到端说明
- 尚未解决的问题——未处理的例外、超出范围但已知过时的部分,以及无法消除的风险
在 Mode 2 中,这份报告还应包含价值分析表和修改前后的指标。
第 4 点最容易被遗漏,但它会产生持续影响:指出删除的派生值,能让读者明白为什么不应将它加回来。
反模式
| 模式 | 问题 | 应采取的做法 |
|---|---|---|
| 盲目删除 | 丢失信息 | 先逐节进行价值分析 |
| 全部保留 | 没有实现精简 | 严格应用价值标准 |
| 存在多个真实来源 | 日后必然出现分歧 | 只保留一个权威位置,其他位置使用链接 |
| 孤立引用 | 链接失效 | 移动内容后更新所有引用 |
| 持续保留派生值 | 会悄然过时;没有任何提示 | 漂移测试问题 1——不要写入 |
| 链接到派生值 | 脚手架本身也会过时,还会让人误以为两处内容保持一致 | 同上——该值本不应存在 |
| 修补前提已失效的文档 | 内部一致,整体错误 | 将其归档(处置步骤 4) |
| 只更新显而易见的出现位置 | 隐蔽的副本会留存下来,之后与之矛盾 | 编辑前先搜索(步骤 5) |
| 记录尚未实现的期望 | 文档从一开始就与实际情况不符 | 按照步骤 3 执行:只修复已确立且已获授权的行为;否则报告这一分歧 |
| 无边界地进行“顺手清理” | 差异难以审查,真正的更改也会被淹没 | 有范围的清理(步骤 6) |
输出产物
一次成功的检查会产生:
- 修正后的文档——每项事实都只有一个权威位置
- 价值分析(Mode 2)——逐节说明理由
- 修改前后的指标(Mode 2)——减少的行数、保留的价值
- 搜索证据——运行过的命令及其计数,以便核查相关声明
- 更新后的引用——主文档或 README 中有指向新位置的链接,且所有指向已移动内容的链接都能正常访问
下一步
文档保持一致后,自然可以继续进行以下工作:
Docs are now consistent — [N] files updated, [M] derived values removed.
Options:
A) Convert the recurring parts into a project rule — if the same drift keeps
recurring, the fix is a rule in the project's main doc, not another cleanup pass
B) Render or publish the result — if these docs have a build/publish step
C) Nothing further — the docs are the deliverable