编写高质量的规格说明
你很少从零开始编写规格说明。通常的做法是用自然语言描述变更,/opsx:propose 会起草需求和场景,然后你对其进行优化和完善。本页主要关注最后这一步——什么是“高质量”,以及如何引导 AI 达到这一标准。
它是 审查变更 的配套文档:审查是为了发现草稿中的薄弱环节,而编写则是了解强项是由什么构成的。
规格说明描述的是行为,而非代码
规格说明阐述你的系统 做什么,且这些内容必须是任何人都能验证的——而不是它如何构建。它由 需求(行为声明)和 场景(证明这些行为的具体示例)组成。
### 需求:会话超时
系统在用户无操作 30 分钟后应使会话过期。
#### 场景:空闲超时
- 给定一个已认证的会话
- 当经过 30 分钟且无任何活动
- 则会话失效,用户必须重新认证将 怎么做 ——队列、库、表结构等实现细节——保留在 design.md 或代码中。当行为和实现混合在一个需求中时,该需求将变得不可测试,并且在代码更改的那一刻起就会过时。
什么样的需求是好的
一个好的需求只描述一种行为,表述得如此直白,以至于你可以把它交给其他人去测试。
一条语句,一个
SHALL/MUST。 如果一个需求包含三个“此外”子句,那它实际上是三个需求。请将其拆分。可观察。 代码之外的人应该能够判断其是否成立。“当上传超过 10 MB 时,系统应显示错误横幅”是可观察的。“系统应优雅地处理大文件上传”则不是。
恰当的强度。 OpenSpec 使用 RFC 2119 关键词,它们有不同的含义:
关键词 含义 MUST/SHALL硬性要求。不可协商。 SHOULD强烈建议,但允许有正当理由的例外。 MAY真正的可选。 默认情况下使用
MUST/SHALL。仅在你真正意指“除非有充分的理由不这样做”时才使用SHOULD。
检验一个需求的标准是:一个从未见过代码的测试人员能否判断它是否通过? 如果不能,则需要进一步细化。
什么样的场景是好的
场景是需求体现价值的地方。每个场景都是一个具体的 GIVEN / WHEN / THEN,可以转化为自动化测试。
- 它应验证其对应的需求。 仅仅用不同的词语复述需求并不能测试任何东西。请使其成为具有特定结果的具体情况。
- 覆盖重要的用例,而不仅仅是正常路径。 有效的登录很容易。空输入、过期的令牌、重复点击、出错的情况——这些地方潜伏着 bug,也是场景最有价值的地方。
- 在标题中命名该用例。 “场景:拒绝过期的令牌”能让审阅者一目了然地知道覆盖了什么;而“场景:测试 2”则不能。
一个有用的习惯:在批准之前,问自己 我最不希望看到哪个用例被破坏? ——并确保有一个场景明确指出了它。
选择合适的变更类型
变更使用三种章节类型来描述对规格说明的编辑。正确使用它们可以确保归档后的规格说明保持真实准确:
## ADDED Requirements(新增需求) —— 以前不存在的新行为。## MODIFIED Requirements(修改需求) —— 已经存在并正在发生变化的行为。包括完整的新版本;简短的变更说明有助于审阅者理解。## REMOVED Requirements(移除需求) —— 即将消失的行为,并附有一行原因说明。
在归档时,ADDED 会被追加到主规格说明中,MODIFIED 会替换旧版本,REMOVED 会从其中删除。如果移除了某项功能拥有的最后一个需求,则该功能即告退休:为了避免留下空的规格说明,归档操作会删除 openspec/specs/<capability>/spec.md。由于这是唯一一步涉及删除文件的归档操作,因此需要显式请求——在变更的 .openspec.yaml 文件中添加 retire_capabilities: true,该文件还需要包含 schema:。如果没有它,归档将中止并提示你。退休操作会删除整个文件,因此如果规格说明中包含标题、## Purpose 和需求块之外的任何内容(例如 ## Notes 部分或需求下的注释),归档也会拒绝执行。中止消息会指出这些行;请将它们移至 ## Purpose 或需求中,或手动删除规格说明。对于调用者的签出目录中的规格说明,归档输出还会指明恢复已提交文件的 git checkout 命令;选定的存储库会提供针对签出范围的恢复指导。如果你将一个真实的变更标记为 ADDED,最终会出现两个相互竞争的需求;如果你将新行为描述为 MODIFIED,则没有东西可以替换。如有疑问,请打开当前的规格说明,查看该需求是否已存在。
还有一个章节值得了解。当你的变更创建了一个尚不存在的功能时,请以 ## Purpose 开头——用一两句话说明该功能的用途。归档会将其用作其创建的主规格说明的目的;如果跳过此步骤,你将得到一个需要手动填写的 TBD 占位符。现有的规格说明已经有了目的,因此变更中的目的会被忽略——直接编辑 openspec/specs/<capability-path>/spec.md 以更改目的。在这里,<capability-path> 是相对于 specs/ 的目录,例如扁平化项目中的 user-auth 或按领域组织的项目中的 identity/user-auth。
适度控制变更规模
最常见的作者错误并不是措辞不当的需求,而是一个试图包含三个变更的变更。
一个好的变更只有一个意图,可以用一句话概括。 “添加深色模式切换。” “对登录端点进行速率限制。” “将会话从 Cookie 迁移出去。” 如果描述变更需要大量的“此外”,那就是拆分它的信号。
变更过大的迹象:
- 提案的范围读起来像是一系列无关的功能列表。
- 审阅它将花费一下午的时间,所以没人会去做。
- 两个人无法在不冲突的情况下同时处理它。
- 一半的任务可以独立发布。
较小的变更更容易审阅,更容易在一次专注的会话中构建,并且在六个月后归档成为唯一留存物时也更容易推理。你始终可以并行运行多个变更——参见 编辑与迭代 和 工作流。
相反的情况也会发生:一行拼写错误的修复不需要三个需求和设计文档。根据风险等级匹配相应的流程复杂度。
如何引导 AI 生成良好的草稿
因为 /opsx:propose 负责第一版草稿,所以你得到的结果质量取决于你输入的质量。你不必手动编写需求——你需要很好地引导 AI:
- 陈述意图和边界。 “添加一个在首次加载时跟随操作系统设置的深色模式切换——不要触碰现有的主题 API。” 范围外的部分与范围内的部分同样重要。
- 指明你关心的用例。 “确保有一个针对已经手动选择主题的用户场景。” AI 会覆盖你指出的内容。
- 然后进行编辑。 它是纯 Markdown。收紧模糊的
SHALL,删除没有测试价值的场景,添加它遗漏的用例——或者要求 AI 来做:“超时需求很模糊,将其固定为 30 分钟。”
起草、打磨、重复。经过几轮这样的过程,你会得到一份值得信赖的规格说明,这正是我们的目标。