本页判断标准

只看仓库里写下的规约、不看对话,助手能不能产出要的东西?验收的人能不能照着逐条判对错?

两个都答「能」,规约才算立住。答「不能」的部分,后面五层补不了——先补规约,再动代码。

为什么先写规约

助手指正在用的 AI 编程工具(Cursor / trae / Claude Code);它只按写下的文字执行,不猜意图。只写在对话里的内容,下一次改动读不到。下表从左到右:症状 → 代价 → 该写进哪个文件。

症状代价写进哪里
两人对同一字段用了不同名字合并时接口对不上,返工 1 次字段名写进本次改动的增量(delta),随合并请求评审
同一处逻辑两周内被改了三次说不清原先的验收标准是什么以基线 openspec/specs/ 为准;改它 = 改规约,留一次版本记录
助手产出「看起来对」但没人能验收评审只能逐行读代码每条 Requirement 配一个可执行的检查,放进质量门禁的 make check
新成员接手要先问一圈前两周产出接近零验收标准写进 openspec/specs/,新成员读文件,不用挨个问
需求讨论只留在群里三个月后答不出「为什么这么做」决定写进 openspec/changes/<change-id>/design.md

两种做法差在哪

左列不是「不规范」,是没有验收标准。

氛围编程规约驱动
起点边聊边改,需求停在对话里先立项,需求进 openspec/changes/
助手拿到什么当次对话的几句话仓库里的规约原文,加上 AGENTS.md 等上下文文件
什么算做完看起来对规约里的场景逐条通过
复查看什么逐行读助手的改动差异先读规约,再看实现是否照规约
换人接手要重新问一遍读 openspec/specs/
出问题时不知道原先想要什么规约写着原先要什么,差在哪能定位到具体场景(Scenario)
多轮改动每轮验收标准都可能变,且没痕迹验收标准变了要改规约,改动留在版本历史里

一次改动 = 一份 CHANGE

「一次改动」= 一个 CHANGE,就是 openspec/changes/ 下的一个目录。规约用 OpenSpec 写,不手搓目录:仓库跑一次 openspec init,之后每开一次改动执行 /opsx-propose,四件套由它生成。四件套是 openspec/changes/<change-id>/ 下的 proposal.md、增量 specs/<capability>/spec.md、design.md、tasks.md,各回答一个别人会问的问题,缺一件不算立项。

文件回答什么问题
proposal.md这次解决什么问题;这次明确不做哪些
specs/<capability>/spec.md行为是什么:每条 Requirement 配若干 Scenario,写成 Given / When / Then
design.md为什么选这个方案、舍弃了什么
tasks.md拆成哪些可勾选的步骤,一条一步

目录结构(一个变更一个目录)

目录名是 change-id(例:class-isolation),不带日期。四件套之外只有一份验收记录 verify.md,它是本课要求补的,不属于 OpenSpec 的四件套。

openspec/
  config.yaml                            # openspec init 生成:给 AI 看的上下文与规则
  changes/                               # 进行中,可能被否掉
    class-isolation/                     # change-id,不带日期
      proposal.md
      design.md
      tasks.md
      verify.md                          # 本课补充:验收后补,场景逐条结论
      specs/
        class-isolation/
          spec.md                        # 增量:本次新增或修改的 Requirement
    archive/                             # 已完成,只作追溯
      2026-09-26-login-lockout/          # 归档时由 OpenSpec 加上日期前缀
  specs/                                 # 当前行为基线,归档后并入
    auth/
      spec.md
    class-isolation/
      spec.md

openspec/changes/class-isolation/proposal.md

「这次不做」是提案里最有用的一节:评审先看它,才知道边界画在哪。

# 提案:班级资料隔离

- change-id:class-isolation
- 提出人:张(后端)
- 日期:2026-10-03
- 关联 issue:#42

## 问题

学生 A 用检索接口能查到二班的材料。已在测试环境复现两次,见 issue #42。

## 这次做

1. 检索接口按班级过滤,非本班材料不进结果集。
2. 越权尝试记一条 warning 日志,字段含用户、班级、查询词。

## 这次不做

1. 不改管理员跨班查看——管理员本来就应当能看。
2. 不改已上传材料的可见性设置。
3. 不做全文检索的排序优化。

## 边界

只动 /api/kb/search 与它的数据访问函数。前端不感知这次改动,
不改任何响应字段名。

openspec/changes/class-isolation/specs/class-isolation/spec.md

增量只写本次新增或修改的部分,不重抄整份规约;正常路径与失败路径平级。完整写法见可执行规约怎么写。

## ADDED Requirements

### Requirement: 检索按班级隔离

学生只能检索到本班的材料。

#### Scenario: 学生检索他班资料
- Given 学生 A 属于一班,且二班已上传材料
- When  学生 A 用关键词检索二班的材料
- Then  返回结果为空,且响应中不含二班文档标题
- And   服务端记录一条 warning,含用户、班级、查询词

#### Scenario: 直接按 id 取他班文档
- Given 学生 A 属于一班,文档 42 属于二班
- When  学生 A 请求 /api/kb/doc/42
- Then  返回 404,且不返回任何正文

#### Scenario: 未登录访问检索接口
- Given 请求未携带会话
- When  调用 /api/kb/search
- Then  返回 401,且不返回任何文档片段

openspec/changes/class-isolation/design.md

只记「需要解释的决定」:写「为什么」和「舍弃了什么」。

# 设计:检索按班级隔离

- 状态:已定,2026-10-03 冻结

## 决定

在数据访问层加班级过滤,不在业务层过滤。

## 为什么

业务层过滤要先取全量结果再筛,代价随材料总数增长;数据访问层过滤走
索引,且将来加缓存时缓存键天然带班级。代价是这段查询与数据库绑定,
本项目只用 PostgreSQL,可以接受。

## 舍弃的方案

1. 在 controller 里过滤:有三个调用点,漏一处就漏一个洞。
2. 按班级建视图分表:改动面太大,本次不做。

openspec/changes/class-isolation/tasks.md

勾选项写不进一句话的,说明还没拆开;每条都要能指到文件或命令。

# 任务:检索按班级隔离

- [ ] 1. 在 app/kb/data.py 的检索查询里加 class_id 过滤条件
- [ ] 2. 越权命中时写 warning 日志,字段:user_id / class_id / query
- [ ] 3. 补上面三个 Scenario 的测试,接进 make check
- [ ] 4. 跑 openspec validate class-isolation --strict,无 error
- [ ] 5. 在同一次合并请求里提交规约与代码
- [ ] 6. 评审通过后补 verify.md,归档:增量并入 openspec/specs/

助手交出来的样子

## 这次不做
1. 不涉及其他模块的改动。             ← 讲了等于没讲

### Scenario: 检索结果正确
- Given 用户发起检索
- When  调用 /api/kb/search
- Then  返回正确的检索结果             ← 不可判

$ git log --oneline -2
7c2e4f3 实现按班级过滤检索             ← 实现在前
b41d90a 补规约                         ← 规约在后

认它的办法:看规约的提交是不是早于实现的提交。上面这份四件套齐全、格式工整,但它是实现完了才补的——照着已经写好的代码倒写出来的规约,永远全绿,因为它描述的正是已经发生的事。倒写的规约挡不住任何东西:它和代码一致是构造出来的,不是判出来的。

什么时候不用开 CHANGE

这套做法只为会改变行为的改动付成本(多半天写规约、多一轮评审)。把所有改动都套一遍流程,结果是全组绕过流程,比不做流程更糟。

改动要不要开 CHANGE为什么
改错别字、改代码格式不用,直接提交没有行为变化,规约不需要动
依赖版本升级且无接口变化不用,直接提交 + 跑 make check验收标准没变,检查能覆盖
改一个超时时间、改一个阈值要开阈值写在基线 openspec/specs/ 里,改它就得留一次版本记录
加按钮、改文案不用无行为变化,除非文案本身是验收项
谁都说不清这次要改什么先写 proposal.md,写不出就不动手写不出「这次不做」,说明边界还没想清楚