本页判断标准

评审规约的人,能不能在不看代码的情况下答出「这份规约写全了没有」?

答「不能」,通常是评审在看实现细节,而不是在看覆盖面和期望值。按下面的清单逐条回答「是 / 否」,比自由讨论更快。

规约走合并请求

把规约和代码放进同一次合并请求:进仓库、开合并请求、至少另一人看过才合。不能既当提出人又当评审人——自己写自己批的规约,等于没有第二个人读过。

评审代码评审规约
要回答的问题这是不是一段正确的实现?这是不是一份完整、无歧义的行为描述?
看什么改动差异、分支覆盖、异常处理场景是否覆盖正常与失败两类、期望值是否具体
通过标准行为正确,检查通过另一个人能只照它写出测试
挡回时说什么「这里会崩」「这里漏了判断」「这条 Then 不是具体值」「这类失败路径没写」
顺序先读差异,再跑检查先读规约,再抽读与规约对应的那几处代码

评审时逐条追问什么

五类追问对应五类容易漏掉的东西。每一条「动作」列都直接给出要做的那一步,评审意见才不会停在「再改改」。

追问答「否」说明什么动作
这次明确不做什么?不做的事情写下来了吗?边界没画,实现的人会顺手多做一块补 proposal.md 的「这次不做」,至少 3 条
正常、越权、非法输入、依赖失败,哪一类还空着?有一整类失败路径没人想过按可执行规约里的四类失败路径各补 1 个场景
每条 Then 是不是具体值?期望值不可判断,验收时会吵架状态码写数字,字段写名字,比例写阈值
这条要求对应哪条检查?现在跑得起来吗?规约只是一份文档接进 make check,跑不出红就先别合
这个方案要引入什么?有没有更小的做法?成本没被算过写进 design.md 的「舍弃的方案」,并说明代价

一份可抄的评审清单

上面那些追问已经写成能回答「是 / 否」的问句,评审时不用凭印象。规则只有两条:遇到一个「否」就挡回,并在合并请求里写明是哪一条;不适用的条目要写「不适用 + 理由」,不能留空——留空等于没人看过。

docs/checklists/spec-review.md

评审人把这份文件贴在合并请求里逐条回答,答完再点批准。

# 规约评审清单

- 适用:openspec/changes/<change-id>/ 下的规约改动
- 用法:每条回答「是」或「否」;一个「否」即挡回,并写明是哪一条
- 不适用:写「不适用 + 理由」,不能留空

## 提案

1. proposal.md 里有没有写「这次不做」?
2. 「这次做」的每一条能不能指到接口、页面或数据表?
3. 有没有哪一条既不在「做」也不在「不做」里?

## 场景

4. 每个 Scenario 都有 Given、When、Then 三段?
5. 每条 Then 是不是一个具体值(状态码、字段名、数量、阈值)?
6. 越权、非法输入、依赖失败、状态不合法,四类里有没有哪一类完全没写?
7. 有没有哪条要求只有一句陈述、没有对应 Scenario?
8. 「友好」「准确」「清晰」这类没有阈值的词,出现过吗?
9. 文件顶部有没有写覆盖接口与检查入口?

## 可验收

10. 每个 Scenario 能不能对应到一条测试或一条查询?
11. 现在跑得动吗?跑不过的那几条,是本次要实现,还是已有回归也红了?
12. 一票否决项(越权泄漏、编造出处、未登录返回片段)有没有写成 0 容忍?

## 三方一致

13. 规约、tasks.md、代码改动是否在同一次合并请求里?
14. design.md 里被舍弃的方案,理由下一个人看得懂吗?
15. 有没有和 openspec/specs/ 基线的现有条款冲突?冲突怎么处理的写下来了吗?

助手交出来的样子

## 评审结论
1. 提案写了「这次不做」?            是
4. 每个 Scenario 都有三段?          是
6. 四类失败路径都写了?              是
...
15. 与基线冲突怎么处理的?           是

评审意见:LGTM,规约写得很完整。

认它的办法:挑一条答「是」的,让它指到规约里的具体一行。15 条全「是」的清单通常一条都指不到——这份规约其实只有正常路径,而助手照着规约写结论时,把「规约里写了什么」当成了「规约写全了什么」,这两件事在只有正常路径的规约上完全一样。所以清单要求「不适用」也要写理由:留空就说明没人看过,全「是」和没人读过分辨不出来。

规约与代码同步

规约描述行为,代码实现行为。在两者不一致时说「先这样,回头补」,回头不会来,下一个读这份规约的人拿到的是错的验收标准。

可以这么做 改行为,就在同一次提交里改规约;改规约,就在提交信息里写清哪一段行为跟着变;发现规约过期,就单独开一次提交清掉,并在变更记录里加一行。
不要这么做 先合并代码、规约「下次一起补」;在合并请求的评论里口头改验收标准;为了少改一处,把规约改成迁就当前代码的样子。

还有一类不一致更隐蔽:行为没变,但规约里的说法过期了(接口改名、队列换了实现)。每月清一次这种条目,一次清一批,不要攒到季度末。

changes/ 与 specs/ 各放什么

负责的这个变更验收通过之后,把增量并回基线,再把变更目录归档。这样仓库里始终有一份「当前系统是什么行为」的规约,而不是一堆历史提案。两个目录的差别只有一条:changes/ 里的东西可能被否掉,specs/ 里的东西一定是当前真的行为。

位置放什么谁写什么时候变存活多久
openspec/changes/<change-id>/进行中的提案:四件套 + verify.md提出人立项时创建,验收后移走通常 ≤ 2 周
openspec/specs/当前行为基线,按接口或模块分文件提出人验收后并入每次变更验收通过时更新长期,随行为变化更新
openspec/changes/archive/已完成的提案,只作追溯提出人验收后从 openspec/changes/ 移入长期留存,不再修改

目录示例

被否掉的提案也要归档:在 proposal.md 末尾加一行「结论:不采纳,原因……」,再移进 openspec/changes/archive/。下一个翻仓库的人看到为什么不这么做,才不会把同一个方案再提一次。

openspec/
  config.yaml                           # openspec init 生成:给 AI 看的上下文与规则
  changes/                              # 进行中,可能被否掉
    async-grading/                      # change-id,不带日期
      proposal.md
      design.md
      tasks.md
      verify.md
      specs/
        grading/
          spec.md
    archive/                            # 已完成,只作追溯
      2026-10-03-class-isolation/       # 日期由 OpenSpec 归档时加
        proposal.md
        design.md
        tasks.md
        verify.md
        specs/
          class-isolation/
            spec.md
  specs/                                # 当前基线,等于「系统现在是这样」
    auth/
      spec.md
    class-isolation/
      spec.md
    grading/
      spec.md                           # 异步批改验收后并进来

边界:不要在 specs/ 里留历史版本。历史在版本控制里查,不在文件里堆——同一份文件里叠着三个版本的条款,读的人分不清哪条现在有效。要查某个条款什么时候变的,用下面的版本记录。

规约变更的版本记录

版本控制记得住「文件变了」,记不住「哪一条变了、为什么」。所以在每个 specs/ 文件顶部放一张变更记录表,四列缺一不可:日期、谁、改了哪一条、为什么。

openspec/specs/class-isolation/spec.md(顶部)

表格只记「改变行为」的改动,改错别字不记。

# 班级资料隔离

- 状态:已定,2026-10-03
- 覆盖接口:POST /api/kb/search、GET /api/kb/doc/{id}
- 检查入口:make check(pytest tests/spec/test_class_isolation.py)

## 变更记录

| 日期 | 谁 | 改了哪一条 | 为什么 |
|---|---|---|---|
| 2026-10-03 | 张 | 新增 Requirement「检索按班级隔离」及 3 个 Scenario | issue #42,跨班可检索 |
| 2026-10-17 | 李 | Scenario「未登录」期望由 403 改为 401 | 与 auth.md 统一 |
| 2026-10-24 | 张 | 删除 Scenario「管理员跨班检索」 | 与提案的「这次不做」重复,管理员行为写在 auth.md |

每条记录都要能对上一次提交。查法两条:git log --oneline -- openspec/specs/class-isolation/spec.md 看提交,git blame openspec/specs/class-isolation/spec.md 看某一行是哪个提交加的。表格和提交对不上时,以提交为准——表格是给人读的索引,提交才是记录。