首页
/ 文档与代码同步演进:get-shit-done 的 `lint:docs` 文档强制校验机制(PR 3213)深度解析

文档与代码同步演进:get-shit-done 的 `lint:docs` 文档强制校验机制(PR 3213)深度解析

2026-09-08 15:54:43作者:昌雅子Ethen

本技术指南围绕 get-shit-done(GSD)仓库在 PR #3213 中引入的 lint:docs 文档强制校验机制展开,系统讲解它如何在 CI 层强制任何新增、变更、废弃或移除用户可见行为的 PR 必须同步修改 docs/ 下的文档。读者将掌握该 lint 的判定规则、两类带审计痕迹的豁免通道(no-docs 标签与 docs-exempt 片段标记)、坏 frontmatter 的 fail-closed 兜底策略,以及如何在本仓库中本地运行与验证该机制。

一、机制背景:为什么一个 meta 提示工具需要"文档强制"门禁

get-shit-done 是一个面向 Claude Code 等 AI 编程助手的轻量级 meta-prompting 与规格驱动开发(spec-driven development)系统。这类项目以大量文档(docs/ 下的用户指南、命令参考、配置说明、ADR 架构决策记录)作为运行时知识的核心载体——Agent 通过读取这些文档来理解命令契约、工作流与配置项。因此,文档一旦与代码脱节,下游所有消费方(开发者、Agent、LLM)都会被误导

项目采用 changeset 机制管理变更记录:每个有用户可见影响的 PR 在 .changeset/ 下放置一个 <随机词>.md 碎片,发版时再聚合进 CHANGELOG.md。该机制解决了"多个 PR 同时编辑同一段 CHANGELOG 导致合并冲突"的痛点(详见 .changeset/README.md)。但它在设计上只约束变更是否被记录,不约束文档是否被同步更新——于是代码悄悄改、文档停留在旧描述的现象就会反复出现。

PR #3213 正是为此而生:新增独立的 lint:docs 门禁,与既有的 changeset 门禁互为补充。前者(scripts/changeset/lint.cjs)只检查 PR 是否携带碎片,后者则接管"带了哪些类型碎片就必须同步改文档"的职责。这一设计也写入了 CONTRIBUTING.md 供贡献者查阅。

二、核心判定规则:哪些碎片类型会触发文档要求

lint 的规则相当克制且经过精心裁剪。核心常量定义在 scripts/lint-docs-required.cjs

const LINT_REASON = Object.freeze({
  OK_NO_TRIGGERING_FRAGMENTS: 'ok_no_triggering_fragments',
  OK_DOCS_UPDATED: 'ok_docs_updated',
  OK_OPT_OUT_LABEL: 'ok_opt_out_label',
  OK_FRAGMENTS_EXEMPT: 'ok_fragments_exempt',
  FAIL_DOCS_MISSING: 'fail_docs_missing',
  FAIL_MALFORMED_FRAGMENT: 'fail_malformed_fragment',
});

// Fragment types that require a docs update.
const TRIGGERING_TYPES = new Set(['Added', 'Changed', 'Deprecated', 'Removed']);

其判定逻辑可归纳为以下要点:

  • 被门禁的类型AddedChangedDeprecatedRemoved。这四类都意味着用户可见行为的增减或改变,理应伴随文档更新。
  • 不被门禁的类型FixedSecurity。两者的语义是"修复回归"或"修复漏洞"——它们恢复的是文档已承诺的行为,而非引入需要新写文档的行为(安全修复若因保密需要不便透露细节,也无需强制留档)。CONTRIBUTING 同时提醒:若修复本身纠正了文档中的错误,仍建议顺手改文档。
  • 文档路径判定docs/ 前缀下的任意文件(包括 docs/adr/docs/agents/ 等嵌套子目录)都算数,由 isDocsFile 辅助函数通过 file.startsWith('docs/') 判断(见 tests/lint-docs-required.test.cjs)。注意:根目录的 README.mdCONTRIBUTING.md不算 docs/ 文档。
  • 碎片路径判定isFragmentPath 只匹配 .changeset/<slug>.md,且显式排除 .changeset/README.md 与嵌套子目录。

lint 的入口是一个纯函数 evaluateLint,签名与职责在源码注释中有完整说明(scripts/lint-docs-required.cjs):

function evaluateLint({ changedFiles, fragments, labels, malformed = [] }) {
  if (malformed.length > 0) {
    return { ok: false, reason: LINT_REASON.FAIL_MALFORMED_FRAGMENT, ... };
  }
  const triggering = fragments.filter((f) => TRIGGERING_TYPES.has(f.type));
  if (triggering.length === 0) {
    return { ok: true, reason: LINT_REASON.OK_NO_TRIGGERING_FRAGMENTS, ... };
  }
  // 1. 所有触发碎片都带 docs-exempt 标记 → OK_FRAGMENTS_EXEMPT(部分豁免失败)
  if (triggering.every(isExemptFragment)) {
    return { ok: true, reason: LINT_REASON.OK_FRAGMENTS_EXEMPT, ... };
  }
  // 2. PR 带 no-docs 标签 → OK_OPT_OUT_LABEL
  if (labels.includes(OPT_OUT_LABEL)) {
    return { ok: true, reason: LINT_REASON.OK_OPT_OUT_LABEL, ... };
  }
  // 3. PR diff 中确实出现了 docs/ 文件 → OK_DOCS_UPDATED
  if (changedFiles.some(isDocsFile)) {
    return { ok: true, reason: LINT_REASON.OK_DOCS_UPDATED, ... };
  }
  // 4. 兜底:以上全不满足 → 失败
  return { ok: false, reason: LINT_REASON.FAIL_DOCS_MISSING, ... };
}

evaluateLint 不做任何文件系统或 git 操作,因此可以像测试那样针对 changedFilesfragmentslabelsmalformed 四类输入穷举判定路径。这种"纯函数核心 + 薄 CLI 外壳"的结构(与 lint:changeset 保持一致)让测试能够断言结构化 verdict 而非自由文本。

三、本地运行与 CLI 行为

仓库在 package.json 中注册了脚本命令:

npm run lint:docs
# 等价于 node scripts/lint-docs-required.cjs

其运行流程如下:

  1. 收集 PR 标签:读取 GITHUB_EVENT_PATH 环境变量指向的事件 JSON,提取 pull_request.labels 中的标签名。
  2. 计算变更文件集:通过 git diff --name-only origin/${base}...HEAD 获取本分支相对基础分支(默认 main,可由 GITHUB_BASE_REF 覆盖)的改动文件。CLI 使用 execFileSync 并以参数数组传值、不经 shell,且 git 自身的引用名校验会拦截元字符,杜绝了恶意 GITHUB_BASE_REF 注入 shell 语法的可能(源码注释对此有明确说明,scripts/lint-docs-required.cjs)。
  3. 解析碎片:对 diff 中每个 .changeset/*.md 调用 parseFragment,产出 { fragments, malformed } 两组数据(已被删除的碎片自动跳过)。
  4. 输出 verdict
    • 通过时打印 ok docs-lint: <reason>exit 0
    • 文档缺失时在 stderr 列出所有触发碎片及其 type,并提示三种出路(改 docs/、加 no-docs 标签、或写 docs-exempt 标记)后 exit 1
    • 存在解析失败的碎片时,打印具体失败原因与 detail,exit 1

CI 亦支持 --json 参数输出结构化诊断(verdictchangedFilesfragmentsmalformedlabels),便于调试或接入其他工具。

四、CI 工作流接线

该 lint 以独立 workflow 运行,配置文件位于 .github/workflows/docs-required.yml。工作流在 PR 的 opened / synchronize / reopened / labeled / unlabeled 事件时触发——监听 labeled/unlabeled 的用意很明确:提交者可以在 CI 失败后补打 no-docs 标签,从而无需重新 push 即可重新触发校验

name: Docs Required
on:
  pull_request:
    types: [opened, synchronize, reopened, labeled, unlabeled]
jobs:
  docs-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: '24'
      - name: Run docs-required lint
        env:
          GITHUB_BASE_REF: ${{ github.base_ref }}
        run: node scripts/lint-docs-required.cjs

两个值得注意的工程细节:fetch-depth: 0 保证能拿到完整历史以执行 origin/${base}...HEAD 三点比较;GITHUB_BASE_REF 显式注入目标分支名,避免硬编码 main 在分支命名不同的 PR 场景下失效。

五、两条带"审计痕迹"的豁免通道

规则只约束"确有必要"的场景。对于确实没有用户可见文档影响的改动(基础设施重写、纯内部重构、仅测试变更、CI 调整),CONTRIBUTING 提供了两条通道——二者的共同底线是必须留下纸面审计痕迹(paper trail)

5.1 全局标签:no-docs

给 PR 打上 no-docs 标签即可整体放行(对应 verdict OK_OPT_OUT_LABEL)。实现上读取 GitHub event JSON 中的标签列表,命中即生效(scripts/lint-docs-required.cjs)。规范同时要求提交者在 PR 里留言解释为何无需文档。标签是 PR 级别的全局豁免,适合整份 PR 都不涉及用户可见行为的场景。

5.2 每碎片标记:<!-- docs-exempt: <reason> -->

当一份 PR 混合了"需文档的改动"与"无需文档的内部改动"时,逐碎片豁免比全局标签更精准。语法是在触发碎片正文末尾单独一行放置:

<!-- docs-exempt: 一句话说明为何该碎片不需要文档 -->

标记的解析在 scripts/changeset/parse.cjsextractDocsExempt seam 完成,使用如下正则:

const DOCS_EXEMPT_RE = /^[ \t]*<!--[ \t]*docs-exempt[ \t]*:[ \t]*(\S[^\r\n>]*?)[ \t]*-->[ \t]*\r?$/im;

解析后的结果以结构化字段 fragment.docsExempt(reason 字符串或 null)暴露给 lint(isExemptFragment 据此判断),同时把标记行从正文中剥除、规整空白(CRLF 感知),从而保证标记永远不会泄漏进 CHANGELOG.md 或 GitHub release notes 的渲染输出——发版序列化器会向正文末行追加 (#PR号) 后缀,若标记残留就会污染发布文案。相应的防护测试在 tests/lint-docs-required.test.cjs 中验证了"标记进入类型化字段、并从 body 中消失"。

该正则与语义有几个刻意设计的安全边界,值得逐条理解:

  • 必须是独立成行:正则锚定 ^...$(多行模式),行内/段落内(例如反引号包裹的语法示例)提及该标记不会被误判为豁免;
  • reason 必填且非空\S 锚点强制 reason 首字符为非空白。裸写 <!-- docs-exempt --><!-- docs-exempt: -->(空 reason)会被拒绝——没有 reason 就没有审计价值;
  • CRLF 兼容:末尾 \r? 吞掉 CRLF 换行的 \r,避免 Windows 作者的碎片留下残差导致 (#NNNN) 后缀被挤到空行;
  • 线性时间安全:reason 字符类 [^\r\n>] 有界,不会在对抗性输入上发生灾难性回溯。

5.3 两种豁免的裁决顺序与"部分豁免失败"原则

evaluateLint 源码可见裁决顺序是:先查每碎片标记(全部命中才放行),再查 no-docs 标签,最后才看是否真的改了 docs/。其中蕴含一条严格原则——部分豁免失败:只要存在一个未带标记的触发碎片,即使其他碎片都标记了 docs-exempt,整份 PR 仍判失败(对应测试 tests/lint-docs-required.test.cjs)。混有 Fixed(不触发)+ Added(触发)的 PR 同理,只有 Added 会被列入触发清单要求文档。

六、fail-closed:坏 frontmatter 无法绕过文档门禁

这是 #3213 在 Codex 协助开发过程中补上的重要安全语义。lint:changeset 只检查碎片的存在性而非有效性,因此若一个 type 应为 Added 的碎片因 frontmatter 错误(缺 pr、缺 type、type 非法)而无法被解析,它可能被静默跳过——恶意或粗心者借此即可绕过文档要求。

docs lint 主动接管了这一责任:任何出现在 diff 中却无法通过 parseFragment 的碎片都会进入 malformed 列表,而 evaluateLintmalformed.length > 0 优先且无条件地判失败,产出 verdict FAIL_MALFORMED_FRAGMENT。这一检查甚至先于 no-docs 标签的判定——测试明确断言"no-docs 标签无法绕过 FAIL_MALFORMED_FRAGMENT"(tests/lint-docs-required.test.cjs),即标签只能豁免"文档缺失",不能豁免"碎片本身写坏了"。此时 CLI 会逐条打印每个失败碎片及其原因(missing_frontmatterinvalid_typemissing_prinvalid_prempty_body 等,枚举见 scripts/changeset/parse.cjs)。

tests 目录中还专门收录了一条回归测试:Added 碎片只写 type 不写 pr 时,从磁盘解析即被判为 missing_pr,再送入 evaluateLint 得到 fail-closed 结果(tests/lint-docs-required.test.cjs)。

七、"该改哪份文档"矩阵与英文规范语言策略

为了让"改文档"落得更实而非敷衍了事,CONTRIBUTING 新增了 Documentation Updates 一节,用一张矩阵明确改动类型 → 应更新的文档

改动类型 必须更新的文档
新命令或新 flag docs/COMMANDS.mddocs/FEATURES.md
命令行为或输出变更 docs/USER-GUIDE.mddocs/COMMANDS.md
配置 / schema 变更 docs/CONFIGURATION.md
架构性变更 docs/ARCHITECTURE.mddocs/adr/
Agent 或 skill 变更 docs/AGENTS.md
移除命令 / flag / 工作流 所有引用过它的文档

配套还有英文规范语言策略docs/ 与根 README.md 的内容必须使用英文书写,英文版本是唯一规范来源(canonical source);而 README.pt-BR.mdREADME.zh-CN.mdREADME.ja-JP.mdREADME.ko-KR.md 属于社区维护的翻译版本,不要求每份 PR 同步更新。

这份矩阵同时被固化进了 PR 模板:feature.mdenhancement.md 都新增了 Documentation 章节(含 Checklist 与到 CONTRIBUTING 对应章节的链接),其中 enhancement.md 明确给出"行为/输出变更 → docs/USER-GUIDE.md 与/或 docs/COMMANDS.md;配置/schema 变更 → docs/CONFIGURATION.md……"的映射指引(.github/PULL_REQUEST_TEMPLATE/enhancement.md)。由于 PR 模板通过 query 参数按类型分发(fix / enhancement / feature),模板正文中点名了 CI 强制逻辑,贡献者在提交流程中就完成了一次自我检查。

八、测试矩阵:verdict 的穷举验证

.changeset/steady-zebras-click.md 中记录的本碎片本身还是"豁免机制自举"的活例子:它 type 为 Added、属于触发类型,却因本质是"文档门禁自身的引导(bootstrap)"而在末尾携带了 docs-exempt 注释——文档层责任落在 CONTRIBUTING.md 与 PR 模板,而 docs/ 保留给面向最终用户的文档。

整套机制的可靠性由 tests/lint-docs-required.test.cjs 提供穷举级保障,测试全部基于 Node 内置的 node:test 运行器(符合项目 Testing Standards,见 CONTRIBUTING.md),大致覆盖五组场景:

  • LINT_REASON 枚举与常量完整性:断言六个 verdict 码与四个触发类型与文档一致;
  • 通过路径:无碎片、仅 Fixed/Security 碎片、Added 碎片伴随 docs/ 变更、docs/adr/ 等嵌套文档路径同样有效;
  • 失败路径:四类触发类型各自无文档变更即失败;混合 Fixed + Added 时只有 Added 触发;
  • 豁免语义:非空 reason 才豁免、空串/纯空白 reason 不豁免(防御 CodeRabbit 发现的 docsExempt: '' 构造漏洞)、部分豁免失败、标签放行;
  • fail-closed:畸形碎片优先于一切判失败、no-docs 标签无法绕过、缺省 malformed 参数向后兼容。

测试统一断言结构化 verdict({ ok, reason, triggering, malformed })而非错误文案,这使得判例即使将来改写提示语也不会破坏测试。

九、在工程实践中运行与落地

如果你是 get-shit-done 仓库的维护者或希望借鉴此模式的贡献者,可以这样使用该机制:

  1. 作为提交者:给改动配齐 changeset 碎片(npm run changeset -- --type <Added|Changed|Deprecated|Removed> --pr <PR号> --body "...",详见 .changeset/README.md),同时按第七节的矩阵更新对应 docs/ 文档;若确属内部改动,二选一打 no-docs 标签或写 docs-exempt 标记并附理由。
  2. 本地预检:push 前运行 npm run lint:docs(在未设置 GITHUB_EVENT_PATH 时标签为空数组),用 --json 查看结构化 verdict 辅助排错。
  3. 接入自有 CI:直接复用 .github/workflows/docs-required.yml 的接线方式——checkout 需带 fetch-depth: 0,并将 GITHUB_BASE_REF 注入为 ${{ github.base_ref }};别忘了监听 labeled/unlabeled 事件让标签豁免即时生效。
  4. 作为设计参考:其"纯函数 verdict + 薄 CLI"的架构、三通道兜底(逐碎片标记 → 标签 → 改文档)、以及"lint 只检查存在性时自行为消费的碎片兜底有效性"的 fail-closed 思路,均可平移到任意基于碎片化变更记录(changeset/conventional commits)的仓库。

总之,lint:docs 与既有的 Changeset Required 门禁构成了一道"变更必记录、记录必落档"的双层防线,让 get-shit-done 这类以文档驱动 Agent 行为的项目始终能保证:文档不是滞后于代码的备忘录,而是与每次用户可见变更同步交付的交付物

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390