约束驱动开发 是做什么的?
概述
本包中的其他技能描述了理想状态的样子。code-review-and-quality 为你提供了五个维度。test-driven-development 为你提供了一个循环。security-and-hardening 为你提供了一个威胁列表。所有这些都存在于智能体阅读的文本中,它们可能被遵循也可能不被遵循,并且没有一项能跨越会话的边界而持久存在。
这项技能的产生结果则不同:一份关于本项目质量门槛的书面记录,包含数字,它比一次对话存活得更久,并且可以被自动化检查。
为什么这很重要?当你自己编写代码时,阅读它就能告诉你代码质量如何。一个智能体一下午的产出比你一周能读的还多,因此判断力必须从你的头脑中转移到围绕循环运行的检查中去。这些检查需要存在,它们需要你真正选择的数字,并且它们需要在足够接近工作的地方触发,以便智能体自行修复其输出。
规范驱动开发告诉你构建什么。测试驱动开发证明它能工作。约束驱动开发则定义了“足够好到可以发布”意味着什么,在任何人开始在拉取请求中争论之前。
何时使用
在以下情况应用此技能:
- 开始一个项目或一个重要的功能,并且没有书面记录的质量门槛
- 用户要求“设置约束”、“添加质量门”、“定义我们的标准”或“阻止智能体产出垃圾”
- 一个智能体正在产生大量的、无人逐行阅读的代码
- CI 有检查,但没人能说出哪些会阻止合并,哪些只是装饰
- 覆盖率、性能或可访问性数字在每个 PR 都会引发争论,而不是一次性决定
- 你即将运行
/build auto或任何自主循环,而介于它和主分支之间的唯一保障是智能体自己编写的测试套件
何时不要使用:
- 项目已经存在
CONSTRAINTS.md并且用户没有在修改它——此时应该阅读并遵循它 - 一次性脚本、临时原型或快速实验
- 用户想要立即进行代码审查(
code-review-and-quality)或建立 CI 流水线(ci-cd-and-automation) - 预期生命周期只有两周的产品市场适配前代码——此时最低标准仍然值得,但其余部分则不必
加载约束
访谈需要一个实时的用户。不要在非交互式上下文中运行它(例如 CI、/loop、自主运行)。如果在这些环境中缺少约束,应用下面的最低标准,记录此操作,并将剩余部分标记给人类处理。
流程
步骤 1:在询问前先检测
永远不要问你可以从文件里读到的东西。在问第一个问题之前,先收集以下信息:
| 信息项 | 查阅位置 |
|---|---|
| 语言和技术栈 | package.json, pyproject.toml, go.mod, Cargo.toml |
| 测试运行器 | 开发依赖项, test 脚本,现有的测试文件 |
| 现有的 linter | eslint.config.*, biome.json, .ruff.toml |
| 当前的覆盖率 | coverage/ 输出,或者运行一次测试套件 |
| CI 配置 | .github/workflows/, .gitlab-ci.yml |
| 智能体框架 | .claude/, .codex/, AGENTS.md |
用两行报告你发现的结果,然后只询问剩余的部分。
步骤 2:四个问题,每个都有默认值
遵循 interview-me 中的“一次一个问题”原则,但有一个变化:这里的每个问题都有一个默认值,因此“我不知道”也是一个完整的答案,仍然能生成可工作的配置。
Q1:除了最低标准,你希望强制执行以下哪些项?
(a) 新代码的测试覆盖率
(b) 安全扫描
(c) 性能预算
(d) 可访问性
(e) 架构边界
推测:(a) 和 (b)——你已经有了测试运行器,并且正在处理用户输入。
不确定时的默认值:(a) 和 (b)。
说出每个选择带来的成本:(c) 和 (d) 需要一个正在运行的 URL,(e) 需要编写一个规则文件。
Q2:当智能体在任务执行过程中某项检查失败时,应该阻止还是仅仅警告?
推测:阻止。因为你是无人值守地运行智能体,而一个无人阅读的警告就只是警告而已。
不确定时的默认值:对于最低标准进行阻止,对于其他所有项,在前两周内进行警告。
Q3:你心中有目标数字,还是希望我测量你当前的状况并维持该标准?
推测:测量。大多数团队没有数字,而一个凭空捏造的数字会被忽略。
不确定时的默认值:测量并维持。请参见下面的“棘轮”机制。
Q4:在智能体将工作交还给你之前,你能容忍的最慢检查是多长时间?
推测:大约 90 秒。再长的话你就会停止运行它。
不确定时的默认值:任务结束时 90 秒,CI 中不限制。
总共四个问题。一个包含十二个问题的引导过程只会产生一份没人理解的配置和一个后悔开始的用户。
步骤 3:编写 CONSTRAINTS.md
在仓库根目录下创建一份文件。任何框架上的任何智能体都能读取它,并且对它的任何修改都会出现在代码审查中,这本来也是它应该出现的地方。
# 约束
最后审查日期:2026-08-08 由 @addy 完成
## 最低标准(始终强制执行,无需设置)
- 不允许新增抑制注释:`@ts-ignore`, `eslint-disable`, `# noqa`, `# type: ignore`
- 不允许未实现的存根:`throw new Error("Not implemented")`,空的 `catch {}`
- 不允许跳过或删除测试,除非在提交信息中说明了原因
- 不允许在源代码中包含密钥
- 此文件不允许为了通过某项更改而被弱化
## 用数字强制执行
| 维度 | 规则 | 检查依据 | 运行阶段 |
|-----------|------|-----------|---------|
| 类型 | 零类型错误 | `tsc --noEmit` | 每次编辑后 |
| 代码风格 | 零错误,使用我们的配置 | `biome check` | 每次编辑后 |
| 密钥 | 源代码中无密钥 | `gitleaks detect --redact` | 每次编辑后 |
| 覆盖率 | 变更行的覆盖率 ≥ 80% | `vitest run --coverage` + git diff | 任务结束时,CI 中 |
| 安全:代码 | 无高级别发现 | `semgrep scan --config p/default` | CI |
| 安全:依赖 | 无高或更高级别 | `osv-scanner scan source -r .` | CI |
| 可访问性 | 零严重或重要问题 | `axe $PREVIEW_URL --tags wcag2a,wcag2aa,wcag21aa` | 预览部署时 |
| 性能 | LCP ≤ 2500ms, CLS ≤ 0.1 | `lighthouse $PREVIEW_URL --output=json` | 预览部署时 |
每一行都指明了产生裁决结果的命令。一个没有数字且此列中没有命令的维度只是一个愿望,而不是一个约束。
## 已测量,尚未强制执行
| 指标 | 当前值 | 方向 |
|--------|-------|-----------|
| 项目整体覆盖率 | 62.4% | 不得下降 |
| 包体积 (main 分支) | 184 kB | 不得增长 |
## 例外情况
| ID | 规则 | 路径 | 原因 | 负责人 | 过期时间 |
|----|------|------|--------|-------|---------|
| W1 | `no-explicit-any` | `src/legacy/**` | 重构工作由 ENG-441 跟踪 | @addy | 2026-11-01 |
然后在 AGENTS.md 和 CLAUDE.md 中添加一行:在编写代码前阅读 CONSTRAINTS.md。不要为了通过某项更改而弱化它。
步骤 4:安装每个维度所需的内容
选择一个维度意味着要安装一些东西。不要只给用户一个数字而没有实现机制,也不要当存在事实上的标准工具时自己发明一个检查器——这里列出的工具之所以被选中,是因为生态系统中的其他所有东西都以它们的规则格式和阈值为目标,因此团队现有的配置可以继续工作。
| 维度 | 工具 | 安装方式 | 运行命令 | 门控条件 |
|---|---|---|---|---|
| 类型 (TS) | tsc | 已存在 | tsc --noEmit | 任何错误 |
| 类型 (Python) | mypy | pip install mypy | mypy . | 任何错误 |
| 代码风格 | 你现有的配置 | 已存在 | eslint . / biome check / ruff check | 任何错误 |
| 覆盖率 (JS) | 你的测试运行器 | 已存在 | vitest run --coverage (或 jest --coverage) | 变更行的覆盖率 |
| 覆盖率 (Python) | pytest-cov | pip install pytest-cov | pytest --cov --cov-report=lcov | 同上 |
| 安全:代码 | Semgrep | pipx install semgrep | semgrep scan --config p/default --config p/owasp-top-ten | 任何高级别发现 |
| 安全:密钥 | gitleaks | brew install gitleaks | gitleaks detect --redact --no-banner | 任何发现 |
| 安全:依赖 | osv-scanner | brew install osv-scanner | osv-scanner scan source -r . | 高级别或更高 |
| 性能:页面 | Lighthouse | npm i -D lighthouse | lighthouse $URL --output=json --quiet | LCP, CLS, 性能分数 |
| 性能:包体积 | size-limit | npm i -D size-limit | size-limit --json | 每项入口的字节预算 |
| 可访问性 | axe-core | npm i -D @axe-core/cli | axe $URL --tags wcag2a,wcag2aa,wcag21aa | 零严重或重要问题 |
| 架构 | dependency-cruiser | npm i -D dependency-cruiser | depcruise --validate src | 任何违规 |
| 断言质量 | Stryker | npm i -D @stryker-mutator/core | stryker run --mutate <changed files> | 变异分数 |
如果你跳过它们,有五件事会给你带来麻烦:
- gitleaks 的
--redact参数不是可选的。 如果没有它,匹配到的密钥会落入智能体的转录文本中,这就是泄漏的密钥最终会出现在日志、摘要或提交信息中的原因。请报告规则和位置,永远不要报告值。 - Lighthouse 和 axe 需要一个 URL。 它们只能针对正在运行的应用程序工作,因此它们属于运行时阶段,针对预览部署或你先启动的本地服务器。如果项目没有可供访问的 URL(例如 CLI 工具、库、桌面应用),请说明情况并放弃该维度,而不是发明一个无法运行的检查。
- 将代价高的检查限制在差异范围内。 对整个仓库运行
stryker run --mutate需要数小时并最终会被关闭;而仅针对变更所影响的文件运行则需要不到一分钟。Semgrep 也是如此,它可以接受一个路径列表。 - 覆盖率不需要第二次运行测试套件。 读取你的套件已经生成的 lcov 覆盖率文件,并将其与
git diff的结果进行交集运算。为了得到一个数字而运行两次测试套件是让人们讨厌这个做法的捷径。 - Semgrep 的注册表规则可以免费运行;但在重新分发前请检查许可证。 如果这关系到你的法务团队,
opengrep是一个即插即用的替代方案,具有相同的规则格式和 JSON 输出。
将每个工具添加到项目自己的脚本中,以便无需智能体即可重现:
{
"scripts": {
"check:fast": "tsc --noEmit && eslint . && gitleaks detect --redact --no-banner",
"check:task": "npm run check:fast && vitest run --coverage",
"check:full": "npm run check:task && semgrep scan --config p/default && osv-scanner scan source -r ."
}
}
这个映射关系比工具本身更重要。check:fast 是在每次编辑后运行的,check:task 在智能体认为任务完成时运行,check:full 在 CI 中运行。
这些命令现在存在于两个地方——CONSTRAINTS.md 的“检查依据”列和这些脚本中。CONSTRAINTS.md 是权威来源:它为每个命令附带了理由,并且会出现在代码审查中。脚本只是方便使用的包装器,必须完全镜像它,而不是第二个真相来源;如果它们开始偏离,以文件为准。
步骤 5:将其连接到开发生命周期
最大的错误是在所有地方运行所有检查。一个拖慢智能体的检查会被关闭,而一个人们关闭了的门禁比没有门禁更糟糕,因为标准看起来仍然存在。
| 阶段 | 命令 | 运行内容 | 预算 |
|---|---|---|---|
| 构建 | /build | 类型检查,代码风格,密钥检查,最低标准 | 5 秒以内,仅限变更文件 |
| 验证 | /test | 相关测试,变更行覆盖率 | 90 秒以内 |
| 审查 | /review | 所有检查,加上下面的守卫 | 几分钟 |
| 发布 | /ship | 方向性检查,无回归 | CI |
两条使得这一切可容忍的规则:
- 限定到差异范围内。 检查此变更影响的代码行,而不是整个仓库。变更行的覆盖率是智能体可以改变的数字;项目整体覆盖率则是它继承来的。
- 成本决定位置。 任何超过几秒的检查都应移出编辑循环。对整个仓库进行变异测试需要数小时;仅针对变更所影响的文件则需要不到一分钟,这决定了检查是人们会运行还是不会运行。
步骤 6:守卫门槛本身
有人会指出,如果智能体既编写代码又编写检查,那么这些检查证明不了什么。这句话说对了一半,并且值得通过工程手段来解决。
智能体不会精心设计巧妙的漏洞。它们遇到红色的检查项,然后选择通往绿色的最便宜路径。在代码审查时,留意差异中的这五种行为:
- 阈值移动了。 预算被降低了,严重等级被降低了,检查项被从快速阶段移除了。将
CONSTRAINTS.md与其在分支点时的状态进行比较。 - 测试变容易了。 添加了
.skip,删除了测试文件,从保留的测试中删除了断言。 - 检查器被静音了。 新增了
@ts-ignore或eslint-disable。有四种抑制操作尤其值得注意,因为它们会关闭你所依赖的检查:istanbul ignore会使代码不被纳入覆盖率统计而非进行测试,Stryker disable会隐藏一个幸存的变异体,nosemgrep和gitleaks:allow对安全发现的作用也是如此。 - 工作未完成。 一个抛出异常的存根,一个将失败转化为静默的空
catch块,一个本应是实现的地方却放着一个TODO。 - 出现了一个例外。 例外表格中出现了一个没有人讨论过的新行。
这些都不需要比 git diff 更复杂的工具。收紧标准应该是静默的;放松标准则应该大声宣告。
与编号的维度不同,最低标准没有自己的事实上的工具,因此被要求执行它的智能体倾向于从头编写一个检查器,而两个智能体会写出两个不同的。作为参考,这五项检查的参考实现随本技能一同提供,位于 references/floor-guard.md(限定于差异范围,退出代码为 0/1/2,模式可适应不同生态系统)。请改编它,而不是重新发明它,原因与每个维度都命名一个事实上的工具相同:这样机制在所有运行中和所有技术栈上都是相同的。
并非所有检查都同等循环论证。 根据一个问题对它们进行排序:智能体能否通过编写无法工作的代码来让这个检查通过?
- 外部检查——axe-core 编码了 WCAG 标准,
osv-scanner读取漏洞数据库,Lighthouse 测量真实的浏览器行为。智能体无法与这些争论。 - 项目级检查——你的代码风格规则,你的层边界。人类拥有这个文件。
- 套件级检查——你自己的测试。最有用,并且是唯一真正循环论证的检查。
一个门槛