约束驱动开发
概述
本包中的其他技能描述了“好”的标准。code-review-and-quality 提供了五个评估维度。test-driven-development 提供了一个开发周期。security-and-hardening 提供了一个威胁列表。所有这些都以代理可以阅读的文本形式存在,代理可能会也可能不会遵循,而且这些信息在对话结束后都会消失。
本技能产生了一些不同的东西:一个关于本项目标准的、包含具体数字的书面记录,它比对话更持久,并且可以通过机械方式进行核查。
为什么这很重要?当你编写代码时,阅读代码能告诉你它是否足够好。一个代理一下午写的代码比你一周能读的还多,因此判断标准必须从你的脑袋中移出,进入在开发循环中运行的检查。这些检查必须存在,它们需要你实际选择的数字,并且它们需要在足够接近工作的地方触发,以便代理能自修复其输出。
规范驱动开发告诉你“构建什么”。测试驱动开发证明它“能工作”。约束驱动开发则定义了“足够好可以发布”意味着什么,免得大家在拉取请求(PR)中争论。
何时使用
在以下情况应用此技能:
- 开始一个项目或一个重要特性,且没有书面质量标准
- 用户要求“设置约束”、“添加质量门禁”、“定义我们的标准”,或“阻止代理发布垃圾代码”
- 代理产出了大量无人逐行阅读的代码
- CI 有检查,但没人能说出哪些检查会阻止合并,哪些只是装饰
- 覆盖率、性能或可访问性数字在每个 PR 中都被争论,而不是一次决定
- 你即将运行
/build auto或任何自主循环,而它和主分支之间唯一的屏障是由同一个代理编写的测试套件
何时不使用:
- 项目已有
CONSTRAINTS.md且用户未要求修改——读取并遵循它 - 一次性脚本、技术验证、一次性原型
- 用户现在就需要代码审查(使用
code-review-and-quality)或构建 CI 流水线(使用ci-cd-and-automation) - 预期生命周期仅两周、尚未找到产品市场契合(PMF)的代码——其下的“底线”仍然值得设置,其余部分则不必
加载约束
访谈需要一个活跃的用户。不要在非交互式上下文(CI、/loop、自主运行)中运行它。如果缺少约束且你处于这些上下文中,请应用下面的“底线”,并注明你已经这样做,然后将剩余部分标记出来留给人工处理。
流程
第一步:先检测,再提问
永远不要问你本来就能读到的东西。在第一个问题之前,先收集:
| 什么 | 查看位置 |
|---|---|
| 语言和技术栈 | 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 |
用两行报告你找到了什么,然后只问剩下的问题。
第二步:四个问题,每个都有一个默认值
遵循 interview-me 中一次一问的原则,但有一个变化:这里的每个问题都有一个默认值,因此“我不知道”是一个完整的回答,并且仍然能产生一个可工作的配置。
Q1:除了底线,你希望强制执行哪些方面?
(a) 新代码的测试覆盖率
(b) 安全扫描
(c) 性能预算
(d) 可访问性
(e) 架构边界
猜测:(a) 和 (b)——你已经有测试运行器,并且你在处理用户输入。
不确定时的默认值:(a) 和 (b)。
说明每个选项的成本:(c) 和 (d) 需要一个运行中的 URL,(e) 需要编写一个规则文件。
Q2:当代理在任务进行中时检查失败,应该阻止还是警告?
猜测:阻止。你在无人值守地运行代理,而一个没人读的警告就等于没有警告。
不确定时的默认值:对“底线”进行阻止,其他所有内容在前两周先发出警告。
Q3:你有目标数字吗,还是我应该测量你现在的状态并守住那条线?
猜测:测量。大多数团队没有数字,而一个凭空编造的数字会被忽略。
不确定时的默认值:测量并守住。请参阅下面的“棘轮效应”。
Q4:在代理交回成果前,你能容忍的最慢检查是多长时间?
猜测:大约 90 秒。再长你就不会再运行它了。
不确定时的默认值:任务结束时 90 秒,CI 中无限制。
最多四个问题。一个十二个问题的问卷只会产生一个没人理解的配置和一个后悔开始的用户。
第三步:编写 CONSTRAINTS.md
一个文件放在仓库根目录。任何代理在任何框架上都能读取它,对其的修改会在审查中显示出来,这正合适。
# 约束
最后审查:2026-08-08,由 @mukhon-core
## 底线(始终强制执行,无需设置)
- 没有新的抑制注释:`@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% | 不得下降 |
| 包大小(主分支) | 184 kB | 不得增长 |
## 异常
| ID | 规则 | 路径 | 原因 | 负责人 | 过期时间 |
|----|------|------|--------|-------|---------|
| W1 | `no-explicit-any` | `src/legacy/**` | 重写在 ENG-441 中跟踪 | @mukhon-core | 2026-11-01 |
然后在 AGENTS.md 和 CLAUDE.md 中添加一行:在编写代码前阅读 CONSTRAINTS.md。不要为了通过变更而削弱其中的标准。
第四步:安装每个维度所需的工具
选择一个维度意味着要安装一些东西。不要只给用户一个数字却没有实现机制,也不要在一个事实上的标准工具存在时自己发明一个检查器——此处列出这些工具是因为它们的规则格式和阈值是生态系统中其他所有东西的目标,这样团队现有的配置就能继续工作。
| 维度 | 工具 | 安装 | 运行 | 门禁条件 |
|---|---|---|---|---|
| 类型(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 工具、库、桌面应用——请说明情况并放弃该维度,而不是发明一个无法运行的检查。
- 将开销大的检查限定在 diff 范围内。 对整个仓库运行
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 是权威来源:它随每个命令一起提供了理由,并且在审查中会显示出来。这些脚本是便捷的包装器,必须镜像它,而不是第二个真相来源;如果它们出现偏差,以文件为准。
第五步:将其接入开发生命周期
最大的错误是在所有地方运行所有检查。一个会拖慢代理的检查会被关闭,而一个被人关闭的门禁比没有门禁更糟糕,因为标准看起来仍然存在。
| 阶段 | 命令 | 运行内容 | 时间预算 |
|---|---|---|---|
| 构建 | /build | 类型、代码风格、秘密、底线 | 5 秒以内,仅针对变更文件 |
| 验证 | /test | 相关测试、变更行的覆盖率 | 90 秒以内 |
| 审查 | /review | 所有内容,加上下面的守卫 | 数分钟 |
| 发布 | /ship | 方向检查,无回归 | CI |
两条规则让这一切可以忍受:
- 限定在 diff 范围内。 检查此变更影响的代码行,而不是整个仓库。变更行的覆盖率是一个代理可以改变的数值;而项目覆盖率是它继承的。
- 成本决定放置位置。 任何超过几秒的检查都应移出编辑循环。对整个仓库进行变异测试需要数小时;但对变更影响的文件则在一分钟内完成,这就是一个人们会运行的检查和一个他们不会运行的检查之间的区别。
第六步:守卫标准本身
有人会指出,如果代理既写代码又写检查,那么检查不能证明任何事情。只说对了一半,值得围绕这个问题进行工程化设计。
代理不会精心设计巧妙的漏洞。它们遇到红色检查时会选择通往绿色状态的最便宜路径。在审查时,请关注 diff 中的这五种操作:
- 阈值移动了。 预算降低、严重性降低、检查从快速阶段移除。将
CONSTRAINTS.md与其在分支点的状态进行比较。 - 测试变得更宽松了。 添加了
.skip、删除了测试文件、从仍然存在的测试中移除了断言。 - 检查器被静音了。 新的
@ts-ignore或eslint-disable。有四个抑制项需要特别关注,因为它们会关闭你所依赖的检查:istanbul ignore将代码从覆盖率中移除而不是测试它,Stryker disable隐藏一个幸存的变异体,nosemgrep和gitleaks:allow则是对安全发现做同样的事情。 - 工作未完成。 一个抛出异常的存根,一个将失败变成沉默的空的
catch,一个占据本应是实现位置的TODO。 - 异常出现了。 没有人讨论过的“异常”表中出现了一个新行。
所有这些都不需要超出 git diff 的工具。收紧标准应该是无声的;放松标准应该是响亮的。
与带数字的维度不同,“底线”本身没有事实上的标准工具,因此要求代理强制执行它时,它往往会从头编写一个检查器,而两个代理会编写两个不同的。本技能在 references/floor-guard.md 中附带了一个这五个检查的参考实现(限定在 diff 范围内,退出码为 0/1/2,可根据生态系统的模式进行适配)。请改编它而不是重新发明,原因与每个维度都指定一个事实上的标准工具相同:确保机制在跨运行和跨技术栈中保持一致。
并非所有检查都具有相同的循环性。 根据一个问题对它们进行排序:代理能否通过编写不工作的代码来通过这些检查?
- 外部——axe-core 编码了 WCAG,
osv-scanner读取漏洞数据库,Lighthouse 在真实的浏览器中进行测量。代理无法与这些争辩。 - 项目——你的 lint 规则,你的层边界。文件由人类拥有。
- 套件——你自己的测试。最有用,并且是唯一真正循环的。
...
[内容因长度截断]
第七步:建立棘轮效应
测量并且...
[内容因长度截断]
底线(最低质量标准)
每个项目,无论多么实验性,都应包含这些。它们不需要安装,不需要配置,并且它们建立的期望是人们在心理上可以接受的:"至少没有把事情搞得更糟。"
- 没有新的抑制注释
- 没有空的 catch 块
- 没有未实现的函数抛出 NotImplementedError
- 没有跳过或删除的测试(除非在提交信息中给出理由)
- 没有硬编码的秘密
这些不保证正确性。它们保证代理必须——用检查者的行话来说——通过检查,而不是在检查周围钻空子。
参考
- floor-guard.md——底线检查的参考实现(shell,适配于 Mac/Linux)
- agent-checklist.md——代理在每次会话开始时,除了此技能说的事情之外还应检查的事项(语言环境、加载的包、分支状态)