Skip to content

编写高质量的规格说明 ​

你很少从零开始编写规格说明。通常的做法是用自然语言描述变更,/opsx:propose 会起草需求和场景,然后你对其进行优化和完善。本页主要关注最后这一步——什么是“高质量”,以及如何引导 AI 达到这一标准。

它是 审查变更 的配套文档:审查是为了发现草稿中的薄弱环节,而编写则是了解强项是由什么构成的。

规格说明描述的是行为,而非代码 ​

规格说明阐述你的系统 做什么,且这些内容必须是任何人都能验证的——而不是它如何构建。它由 需求(行为声明)和 场景(证明这些行为的具体示例)组成。

markdown
### 需求:会话超时
系统在用户无操作 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 分钟。”

起草、打磨、重复。经过几轮这样的过程,你会得到一份值得信赖的规格说明,这正是我们的目标。

快速检查清单 ​

下一步 ​

  • 审查变更 —— 两分钟的快速检查,捕捉遗漏之处。
  • 概念 —— 规格说明、变更和增量的深层模型。
  • 示例与配方 —— 从头到尾的真实变更案例。