【AI Native开发-04】“删除要安全”为什么不是一份可执行的 Spec?
这是“AI Native开发”系列的第 4 篇。
**案例说明:**本文的 API、鉴权和自动化测试契约属于假想全栈项目示例;本博客当前是 Astro 静态站,真实内容变更通过本地 CMS/Git 工作流完成。上一篇解决了“AI 怎么理解项目现状”,这一篇解决“AI 怎么理解你要做什么”。第 1 篇给出了 Spec 质量标尺(五个维度、阈值 7 分),本篇补上那套标尺背后的写作方法:一份 AI 能无歧义执行的 Spec,到底由哪十块东西组成,每块解决哪类失效,以及“非目标”为什么是全篇最值钱的一节。
30 秒看懂这篇
“删除功能要安全”人人看了都同意,却无法据此验收:谁有权删?什么状态能删?失败后文件还在不在?Spec 的工作,就是把这些默认理解摊开。
flowchart TB
A["一句愿望<br/>删除要安全"] --> B["角色与目标"]
B --> C["正常与异常流程"]
C --> D["业务规则与权限"]
D --> E["数据约束与非目标"]
E --> F["REQ 编号的验收标准"]
F --> G["人可评审 · AI 可执行 · 测试可追踪"]本篇给出十项结构和一份完整模板。Spec 既写给人理解,也写给 AI 和测试执行:背景与理由帮助人判断方向,行为规则与验收标准帮助执行者判断对错;不是每一句话都要变成测试,但每一条承诺的行为都应可验证。
**案例边界:**下文统一使用“假想全栈内容服务”,它有运行时 API、鉴权和受控文件存储。它不是当前 Astro 静态博客的部署事实;当前静态博客应采用第 05 篇讨论的本地 CMS/Git 工作流。
先厘清:Spec 和需求文档差在哪
第 0 篇的术语约定里钉过一条:需求文档描述“我们想要什么”,Spec 描述“系统在什么条件下必须表现出什么行为”。这个区别在写作时的操作含义是:
需求文档的句子:
“删除功能要安全,不要误删文章。”
→ 人看了能点头,AI 看了只能猜:什么叫安全?什么算误删?
Spec 的句子:
“当文章 hide 为 false 或缺省(已公开)时调用删除接口,系统返回 409,
文章文件保持存在,列表中仍然可见。”
→ 条件明确、行为明确、可直接翻译成一个测试用例判据一句话:把这句话单独拎出来,一个没参与过讨论的人(或 AI),能不能据此判断某个实现“对”还是“不对”? 能,它是 Spec;不能,它是需求或者愿望。
另一个配套判断:需求文档允许存在“体验流畅”“适当提示”这类词,因为它是给人对齐方向的;Spec 里出现这类词就是漏洞——第 1 篇的 F6(非目标越界)和 F3(一致性缺口)最容易从这种模糊处长出来。
Spec 的十项结构,以及每一项防住哪类失效
一份可执行的 Spec 至少包含下面十块。顺序不是形式,每一块都对应一类具体的“AI 自由发挥”。
| # | 结构项 | 回答的问题 | 主要防住的失效 |
|---|---|---|---|
| 1 | 用户角色 | 谁能用这个功能? | F2 权限默认放行 |
| 2 | 用户目标 | 使用者到底要达成什么? | 目标漂移、做了不解决问题的功能 |
| 3 | 正常流程 | 最顺的路径怎么走? | 主干缺失 |
| 4 | 异常流程 | 每一步失败了怎么办? | F3 一致性缺口(补偿路径缺失) |
| 5 | 业务规则 | 有哪些硬性约束和状态条件? | F1 状态假设 |
| 6 | 权限边界 | 服务端必须拦截什么? | F2、越权 |
| 7 | 数据约束 | 数据长什么样、怎么变化? | F1、数据模型误读 |
| 8 | 验收标准 | 怎样算完成? | F5 验收标准坍缩 |
| 9 | 非目标 | 这次明确不做什么? | F6 非目标越界 |
| 10 | 需求编号 | 每条规则叫什么? | 追溯断裂(测试无法映射) |
下面逐项展开,每一项给“该写什么 + 反例 + 正例”。
1. 用户角色
列出与这个功能有关的所有角色,以及各自能做什么。假想内容服务里角色简单(管理员 / 普通用户 / 未登录访客),但越是简单越要写,避免实现者自行补全权限语义。
反例:“只有管理员能删除。”
正例:
- 管理员:通过服务现有身份系统登录,可删除允许删除的文章
- 普通用户:无删除入口,直接调用删除接口返回 403,且无副作用
- 未登录访客:无删除入口,直接调用删除接口返回 401,且无副作用
- 复用现有身份与角色机制,不另建一套平行权限体系最后一句是需要从项目代码核实的上下文约束(见上一篇 C1),写进 Spec 能防止实现者绕过现有权限模型。
2. 用户目标
用一句话写使用者的真实意图,不是功能描述。
功能描述:“提供删除按钮和删除接口。”
用户目标:“管理员能把写错、废弃的草稿从系统里清掉,
同时绝不可能误删已经对外发布的文章。”目标里藏着验收的重心:这个功能的价值一半在“能删”,另一半在“绝不误删已发布”。后半句会在验收标准里变成高优先级用例。
3. 正常流程
主干路径,步骤化描述,每一步是“谁做什么、系统响应什么”。
1. 管理员在文章列表看到未公开(hide: true)文章旁有「删除」操作
2. 点击「删除」,弹出二次确认,确认文案显示文章标题
3. 管理员确认后,系统删除文章文件
4. 列表刷新,该文章消失
5. 刷新页面 / 重新构建后,文章仍然不存在(删除是持久的)注意第 5 步:静态站点里“列表消失”可能只是前端状态,必须写“重建后仍不存在”才算覆盖了真实行为。这一步对应第 2 篇 Playwright 里“监听 DELETE 请求”之外的持久化验证。
4. 异常流程
主干之外的每一个失败分支。这一节最能体现 Spec 质量——正常流程大家都会写,差距全在异常。
- 删除时文件已被其他人删掉(并发):
返回 404,列表静默移除该项,不报错崩溃
- 二次确认时点取消:不发任何删除请求,文章保持不变
- 删除过程中出错(文件系统/Git 提交失败):
文章保持原状,列表提示“删除失败,请重试”,不留下半删除状态
- 已发布文章调用删除:见业务规则,拒绝F3(一致性缺口)几乎都死在这一节的缺失:AI 实现了“删除成功”,但失败时文章处于什么状态、要不要回滚,全靠猜。
5. 业务规则
这个功能里所有“如果……那么……”的硬约束,尤其状态条件。
- 仅 hide: true 的文章允许删除
- hide 为 false 或缺省(已公开)的文章删除请求一律拒绝,返回 409 Conflict
- 文章是否公开由 frontmatter 的 hide 布尔字段表达,
系统中不存在 status 字符串字段(架构事实,见 pitfalls)
- 删除为物理删除 Markdown 文件,本功能不做回收站/软删除第三条是从前述教学案例的事故里长出来的规则(第 2 篇的 article.status 事故)。把它写进 Spec,而不是只写在 pitfalls,是因为它直接决定实现怎么读状态。
6. 权限边界
明确“前端隐藏”和“服务端拦截”是两件事,且服务端必须独立校验。
- 前端:普通用户与未登录访客看不到删除入口(体验层)
- 服务端:删除接口必须通过现有身份机制独立鉴权
- 未登录请求返回 401;已登录但非管理员返回 403;均不得执行删除
- 拒绝响应的验证不只看状态码,还要直接确认受控存储中的文章仍存在最后一句对应第 2 篇那条关键断言原则:不能只断言状态码,必须验证副作用没发生。 写进 Spec,AI 生成测试时就不会漏掉。
7. 数据约束
涉及的数据结构、字段、变化方式。以下是假想服务采用的文件存储契约:
- 文章 = 受控内容目录中的 <年>/<月>/<slug>.md 文件
- 删除 = 删除该文件,并使后续发布构建不再生成对应页面
- 状态由 frontmatter 的 hide 布尔字段表达,不引入新的 status 字段
- 测试只能操作临时内容目录,不能触碰真实文章8. 验收标准
每条都可判定,最好直接编号(和第 10 项的 REQ 合并管理)。这一节是 F5 的解药:验收标准一旦坍缩成“测试通过”,前面所有漏洞都会隐身。
- AC-1:管理员删除 hide: true 文章 → 存储中文件消失,后续构建不再生成页面
- AC-2:普通用户调用删除接口 → 403,且存储中文件仍存在
- AC-3:删除已公开(hide 非 true)文章 → 409,文件仍存在
- AC-4:确认框点取消 → 不发删除请求,文章不变
- AC-5:并发重复删除 → 一次成功,其余请求返回约定的冲突/不存在结果,无额外副作用第 1 篇讲过空实现测试。要注意:“固定返回 403、什么都不做”的空实现仍能通过 AC-2,所以 AC-2 本身并不能证明测试有效。 若使用“固定返回 200/204 的成功空实现”,AC-2 才会失败。空实现或变异测试只能检查测试对特定错误实现是否敏感,不能替代需求覆盖评审。
9. 非目标(全篇最值钱的一节)
明确这次不做什么。它防 F6(非目标越界),同时是给 AI 的“不许扩大战线”指令。
本次不做:
- 回收站 / 撤销删除 / 软删除
- 批量删除
- 删除操作审计日志
- 已发布文章的“下线”流程(那是另一个功能)
- 任何全局权限模型改造为什么这一节最值钱:AI 的默认倾向是“把它觉得相关的都做了”——你要删除,它顺手给你加回收站、加批量、加操作日志,因为训练数据里“完整的删除功能”都长这样。每一条非目标都是在显式拆除一条它准备自行铺设的铁轨。而且非目标也是给未来的路标:“审计日志”不是永远不做,是这次不做,它会自然成为下一个 Spec 的候选。
10. 需求编号
给每条规则一个稳定 ID(REQ-001…),让 Spec、Plan、测试、Verify 报告能互相引用。
REQ-001 管理员可删除 hide: true 文章
REQ-002 未登录请求返回 401;已登录但非管理员返回 403;两者均无副作用
REQ-003 已公开文章删除返回 409
REQ-004 取消确认不发删除请求
REQ-005 删除后重建仍不存在(人工验收:本地构建确认)
REQ-006 删除失败时列表保持原状态并提示错误
REQ-007 目标文章不存在时返回 404;重复删除不产生额外副作用编号的价值在第 2 篇演示过:需求追踪矩阵 REQ → 用例 → 自动化 → 结论,靠的就是它。没有编号,“每条规则都被测了吗”这个问题永远回答不了。注意 REQ-005 标注人工验收——表里允许有不能自动化的行,但不允许缺行;REQ-007 则把 F4 的重复操作契约固定下来,避免后续文章自行解释。
从一句话到一份 Spec:改写全过程
原始一句话需求:
“给博客加个删除文章功能。”
第一步 补角色与目标:
谁删?管理员。为什么删?清理废弃草稿,绝不误删已发布。
第二步 拆正常流程:
列表 → 二次确认 → 删除文件 → 列表刷新 → 重建后仍消失。
第三步 穷举异常:
并发已删?取消?删除失败?已发布?
(每问一个“如果失败/如果不满足条件会怎样”,就补一条)
第四步 写死业务规则和数据事实:
hide 布尔表达公开状态、物理删除、无 status 字段、无回收站。
第五步 补权限边界:
未登录 401、已登录但非管理员 403,且副作用为零。
第六步 编号 + 准出检查:
每条行为规则给 REQ 号;逐条问“这能写成测试吗?”
写不成测试的行为承诺要改写成可测;背景、理由与非目标不必硬改成测试断言。这个过程里,第三步(穷举异常)和第六步(可测性拷问)是 AI 帮不上太多、必须人来做判断的两步——它们恰好对应第 0 篇责任矩阵里“不可委托给 AI”的那类判断。AI 可以帮你把写好的要点扩写成规整文档,但“漏没漏异常场景”“这条验收够不够硬”,是人在 Gate 1 要签的字。
Spec 准出标准(Gate 1)
写完后逐条过,任何一条不过就退回,不允许带着缺口进 Plan:
[ ] 每条规则都能翻译成至少一个测试用例(可测性)
[ ] 角色、权限、服务端校验明确,前端隐藏 ≠ 安全
[ ] 正常流程 + 每个失败分支都有明确的系统行为
[ ] 状态、数据字段基于真实代码(已核对,不是假设)
[ ] 没有“尽量/适当/友好/流畅”等不可判定词
[ ] 非目标显式列出,且与业务规则不矛盾
[ ] 每条规则有 REQ 编号,可被 Plan 和测试引用
[ ] 两个没参与讨论的人,按这份 Spec 能得出相同验收结论最后一条是元标准:Spec 的读者是“不在场的人”——可能是另一个同事,也可能是三个月后的你自己,更可能是一个对你项目一无所知的 AI。
附录:Spec 模板(可直接复制)
# Spec: <功能名>
## 背景与用户目标
<!-- 一句话真实意图,不是功能清单 -->
## 用户角色
- <角色 A>:<能做什么>
- <角色 B>:<能做什么>
<!-- 写明项目里真实的身份机制,禁止 AI 自建 -->
## 正常流程
1.
2.
## 异常流程
- <失败场景>:<系统行为 / 返回 / 状态>
-
## 业务规则
- <如果……那么……>
-
## 权限边界
- 前端:
- 服务端(独立校验):
- 副作用验证要求:
## 数据约束
- 涉及的数据/文件/字段:
- 数据如何变化:
- 相关架构事实(引用 pitfalls / 决策记录):
## 验收标准(需求编号)
- REQ-001
- REQ-002
<!-- 每条行为承诺都应可验证;关键用例另选能暴露目标错误的变异体,不能指望单一空实现证明全部有效性 -->
## 非目标
- 本次不做:
- 明确不改造的现有模块:
## 待确认问题
- <!-- 写 Spec 时悬而未决的点,进 Plan 前必须清零或转 Spike -->最后一栏“待确认问题”是留给第 5 篇的接口:如果某个问题不是“需求没想清”而是“技术上不知道可不可行”,它不该靠拍脑袋写进 Spec,而该走 Spike 先验证。下一篇就讨论这件事:当技术不确定性挡在 Spec 面前时,怎么用限时实验把它清掉,而不是让它流进实现阶段爆炸。
