主题:第9部分。功能验证:从规范到事实
难度级别:中级
预计学习时间:4-6小时(理论2小时,实践2-4小时)
先决条件: 基本理解Git和分支操作
具备编写测试的经验(单元/集成测试)
熟悉TypeScript和npm基本命令
理解客户端-服务器架构(HTTP、REST)
具备使用markdown文档的经验
建议:熟悉课程第1-8部分的内容(SDD、规范、路线图)
学习目标: 为任何功能编写包含清晰、可验证事实的validation.md,区分事实与散文式愿望
基于功能类型的风险矩阵,选择最优的事实级别(示例、不变量、属性、契约)
应用事实生命周期状态(草稿 → 强制 → 已实现 → 延期)来控制合并前的质量
为审查形成证据包(evidence bundle),包括事实状态、命令痕迹和手动验证结果
使用智能体(Qwen Code)自动比对代码与validation.md,识别规范偏差
概述:本课程部分致力于从文本规范到可验证事实的关键转变——这是一种将意图转化为功能合并就绪证明的机制。规范解释了应该做什么,但本身不能保证正确实现。事实是机器或人类可以无需重新解释即可确认的可执行或明确可验证的陈述。材料涵盖四个事实级别、用于选择适当检查密度的风险矩阵、validation.md文件结构、事实生命周期、CI/CD集成、手动和自动验证,以及为审查形成证据包。特别强调与编写代码的智能体协作:由于模型可能对同一规范做出不同解释,事实成为唯一可靠的合并准入条件。
核心概念: 事实:可执行或明确可验证的陈述,无需解释。示例:npm run typecheck以代码0退出;GET /返回200;响应包含<h1>AgentClinic</h1>。与散文式愿望如"确保页面看起来不错"形成对比。
规范 vs 事实:规范指导开发,解释意图和边界。事实通过提供客观证据允许合并。简短公式:"规范指导。事实允许合并。"
四个事实级别:示例(具体的输入-输出对:一个curl命令,一个测试),不变量(始终为真的陈述:重复运行迁移不会改变模式),属性(验证一类情况:任何超出1..5范围的评分都被拒绝),契约(前置条件 → 操作 → 后置条件:如果会话未认证,则GET /dashboard响应重定向到/login)。
风险矩阵:根据功能类型选择最低充分事实级别的工具。视觉变更需要手动事实;数据迁移需要不变量和属性;授权需要属性和契约;支付需要示例和契约。矩阵的目标是发现遗漏的检查,而非制造官僚主义。
Validation.md:功能验证的核心工件。包含一组事实,每个事实带有命令/检查、预期结果、负责人(自动/手动验证)和状态。不是检查清单,而是用于合并准入的事实集合。
事实生命周期:草稿(已提议,未确定)→ 强制(被接受为功能标准)→ 已实现(有测试、命令或确认)→ 延期(有意识地推迟到未来阶段并附解释)。帮助区分意图和证据。
手动事实:由人类执行但具体且明确的验证。弱示例:"检查界面"。强示例:"在375px宽度下,/feedback页面显示姓名字段、消息字段、提交按钮,无水平滚动和元素重叠"。手动事实对UI是必需的,对语气和可访问性也有用。
证据包(evidence bundle):合并时提供给审查者的紧凑工件:规范链接、带状态的事实列表、命令运行痕迹(退出代码、输出)、手动验证结果、实现过程中做出的决策、提交链接。审查者不应重新运行所有内容——他应该理解作者验证了什么,并在有疑问时进行针对性复核。
人机协同验证:智能体发现机械性不一致;人类评估产品和架构方面:是否符合使命、边界蔓延、未说明的依赖、新开发者对结构的理解程度、风险行为是否有事实支撑、仅留在聊天中的决策。
规范、计划和事实的同步更新:当实现表明需要新结构时(例如,拆分为Layout/Header/Main/Footer),需要同时更新plan.md和validation.md,以便未来的智能体会话不会回到旧解释。
实践练习: 标题:将愿望转化为事实
问题:给定一个散文式验证:"确保反馈表单正常工作,在手机上看起来正常"。将其转化为validation.md的3-4个具体事实,包括自动和手动验证。指明每个事实的级别(示例、不变量、属性、契约或手动事实)。
解答:1. F1 — 示例:curl -X POST http://localhost:3000/feedback -d '{"name":"Test","message":""}'返回400 Bad Request。级别:示例 + 契约(空消息 → 拒绝)。
- F2 — 属性:任何评分超出1..5范围的POST /feedback返回400,不受其他字段影响。级别:属性。
- F3 — 不变量:使用有效数据成功POST /feedback后,feedback表中的记录数恰好增加1,且响应重定向到/feedback。级别:不变量 + 契约。
- F4 — 手动事实:在375px宽度下,/feedback页面显示姓名字段(type='text')、消息字段(textarea)、提交按钮(type='submit')和最近3条记录列表,无水平滚动、元素重叠或文本截断。级别:手动事实。
关键区别:每个事实都包含具体的命令或验证条件、预期结果和排除歧义解释的成功标准。
难度:初级
标题:应用风险矩阵
问题:团队正在开发以下功能:(A) 在首页添加促销横幅,(B) 数据库迁移为users表添加email字段,(C) 新端点POST /payments用于处理支付,(D) 注册表单验证并检查email唯一性。为每个功能确定根据风险矩阵哪些事实级别是必需的,并解释原因。
解答:A — 横幅(视觉/UI变更):示例(正确渲染的HTML)+ 手动事实(视觉层次、移动设备可读性)。没有手动验证无法确认视觉质量。
B — 数据迁移:不变量(重复运行不改变模式/不重复列)+ 属性(迁移对所有现有记录幂等地应用)。示例不足:需要保证大规模数据安全。
C — 支付端点:示例(具体成功支付通过)+ 契约(无效凭证时 → 400并带具体错误;重复幂等键时 → 409)。副作用需要严格的契约。
D — 注册验证:示例(有效数据创建用户)+ 属性(任何重复email被拒绝)+ 契约(缺少必填字段时 → 400并指明缺失字段)。表单验证需要对一类无效输入的属性。
验证:如果迁移B只指定了示例而没有不变量——这是重写validation.md的信号。
难度:中级
标题:创建完整的validation.md
问题:为功能"Hello Hono"——基于Hono的最小化Web应用,包含服务端渲染——设计validation.md。功能包括:安装Hono和tsx、返回含AgentClinic标题的HTML的GET /路由、连接static/style.css、类型检查脚本。使用生命周期状态并指明验证负责人。
解答:```markdown
验证 — Hello Hono
事实集合
F1 — TypeScript编译通过
- 命令:
npm run typecheck - 预期:退出代码0,无类型错误
- 负责人:自动验证(CI + 本地)
- 状态:强制 → 已实现
F2 — 开发服务器启动
- 命令:
npm run dev(后台),然后curl -s http://localhost:3000 - 预期:HTTP 200,Content-Type包含text/html
- 负责人:自动验证
- 状态:强制 → 已实现
F3 — HTML包含标记
- 命令:
curl -s http://localhost:3000 | grep '<h1>AgentClinic</h1>' - 预期:恰好一处匹配,grep退出代码为0
- 负责人:自动验证
- 状态:强制 → 已实现
F4 — 静态文件可访问
- 命令:
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/static/style.css - 预期:HTTP 200,正文包含CSS规则(通过
| head -c 100验证) - 负责人:自动验证
- 状态:强制 → 已实现
F5 — 页面结构语义正确
- 验证:打开响应源代码(
curl -s http://localhost:3000) - 预期:存在
<header>、<main>、<footer>标签且层级正确 - 负责人:开发者手动验证
- 状态:强制 → 已实现
F6 — 移动端视觉完整性
- 验证:DevTools,宽度375px,高度667px
- 预期:标题、主体内容和页脚不重叠;无水平滚动
- 负责人:开发者手动验证
- 状态:强制 → 已实现
就绪标准
- [x] 所有自动事实(F1-F4)在CI中通过
- [x] 手动事实(F5-F6)已在本地验证
- [x] 路线图已更新:Hello Hono阶段标记为完成
- [x] 提交包含specs/和src/的所有变更
注意:F1-F4是自动的、可复现的命令;F5-F6是手动的,但有具体条件和标准。没有任何散文式愿望。
难度:中级
标题:分析规范偏差
问题:验证"反馈表单"功能时测试通过,但实现:(1) 添加了requirements.md中未指定的"电话"字段,(2) 提交后重定向到/success而非/feedback,(3) 未验证消息长度(要求≤500字符)。编写给Qwen Code的分析偏差请求,并描述需要同步更新哪些文件。
解答:给Qwen Code的请求:
/clear 将此分支与@specs/2026-05-01-feedback-form/validation.md进行比较。
展示:
- 已实现并通过的事实;
- 缺少的事实(消息长度验证≤500);
- 模糊且需要重写的事实;
- 实现中未在requirements.md中描述的决策("电话"字段、重定向到/success);
- 规范中的过时陈述。
暂时不要修改文件。
分析后同步更新:
1. **requirements.md**:要么为"电话"字段和/success添加依据,要么恢复到原始要求。
2. **plan.md**:如果添加字段需要新组件,更新组件结构。
3. **validation.md**:
- 添加F-new:POST /feedback且消息>500字符时返回400。
- 更新F-redirect:预期改为/feedback或/success——取决于产品决策。
- 如果保留电话字段则添加事实;否则从实现中移除。
4. **代码**:与更新后的规范和事实保持一致。
原则:实现过程中做出的任何决策都应反映在specs中,而非仅留在聊天或提交消息中。
难度:高级
标题:为合并形成证据包
问题:基于完成的练习,为功能"Hello Hono"准备合并请求描述(证据包)。包含证据包的所有必需元素,并展示审查者如何针对性复核任何事实。
解答:```markdown
## 证据包:Hello Hono
### 规范
- 文件夹:`specs/2026-05-01-hello-hono/`
- requirements.md:记录意图(Hono上的最小化SSR)
- plan.md:Layout/Header/Main/Footer结构
- validation.md:6个事实(见下文)
### 事实状态
| ID | 事实 | 状态 | 确认 |
|---|---|---|---|
| F1 | TypeScript编译通过 | ✅ 已确认 | `npm run typecheck` → exit 0 |
| F2 | 服务器返回200 | ✅ 已确认 | `curl -s -o /dev/null -w '%{http_code}' http://localhost:3000` → 200 |
| F3 | HTML包含<h1>AgentClinic</h1> | ✅ 已确认 | `curl -s http://localhost:3000 \| grep '<h1>AgentClinic</h1>'` → match |
| F4 | static/style.css可访问 | ✅ 已确认 | `curl -s -w '%{http_code}' http://localhost:3000/static/style.css` → 200 |
| F5 | 语义结构 | ✅ 已确认 | 手动验证:源代码包含<<header>、<<main>、<<footer> |
| F6 | 移动端完整性 | ✅ 已确认 | 手动验证:DevTools 375×667,附截图 |
### 命令痕迹
$ npm run typecheck > tsc --noEmit
exit code 0
$ npm run dev & $ curl -s http://localhost:3000 | head -c 200 <!DOCTYPE html><html><head>...<<h1>AgentClinic</h1>...
$ curl -s -w '\nHTTP %{http_code}' http://localhost:3000/static/style.css body { font-family: system-ui; } HTTP 200
### 实现过程中的决策
- 添加Layout组件以保持一致性:plan.md已更新,添加事实F5。
- static/style.css通过Layout中的`<link>`连接,非内联:计划和事实已同步。
### 提交
- `a1b2c3d` — feat: Hono setup with SSR
- `e4f5g6h` — feat: Layout/Header/Main/Footer structure
- `i7j8k9l` — docs: validation.md and roadmap update
### 审查者针对性复核
快速验证所有自动事实:
npm install && npm run typecheck && npm run dev & sleep 2 && curl -s http://localhost:3000 | grep -q 'AgentClinic' && echo 'F3 OK'
核心价值:审查者在30秒内理解作者验证了什么,可以接受或运行一条命令进行复核,无需从头复现。
难度:高级
案例研究: 标题:无不变量的迁移偏差:初创公司数据丢失
场景:4人团队为诊所开发SaaS平台。"数据迁移"功能添加带有users外键的patient_records表。开发者准备migration.sql并在空数据库上本地验证一次——运行正常。validation.md中只有一个事实:"示例:迁移在空数据库上成功应用"。
挑战:合并到staging后,发现由于CI脚本错误导致迁移重复运行,创建了重复的列patient_records_id_1并破坏了索引。生产部署推迟3天。回滚需要DBA手动干预。问题:缺少"重复运行不改变模式"的不变量和"迁移对含现有数据的数据库幂等"的属性。
解决方案:团队为所有迁移引入强制性风险矩阵。为patient_records重写validation.md:
- F1(不变量):
npm run migrate:up && npm run migrate:up→ 模式不变,两次退出代码均为0。 - F2(属性):对于含10K+用户的数据库,迁移在<<30秒内完成且不停锁表。
- F3(示例):迁移后
SELECT COUNT(*) FROM patient_records对新诊所返回0,对已迁移的正确计数。 - 添加CI步骤:两次运行迁移并检查
pg_dump --schema-only | diff -。
同时更新plan.md:将迁移分为pre-deploy(安全)和post-deploy(需监控)。
结果:迁移验证时间从2小时手动检查缩短到5分钟自动验证。实施后8个月内未发生迁移事件。团队能够从每两周1次部署提升到每周3次。审查者信任证据包,无原因不再本地重新运行迁移。
经验教训: 风险矩阵不是官僚主义——它发现盲区:只有一个示例而无不变量的迁移是经典反模式
迁移的"幂等性"不变量比数十个示例更重要:它覆盖所有重复应用场景
CI应验证生产中可怕的场景,而非仅"快乐路径"
证据包降低审查者的认知负荷——他根据工件而非猜测做决策
相关概念: 风险矩阵
不变量
validation.md
证据包
事实生命周期
标题:无手动事实的UI功能:金融科技应用可访问性回归
场景:大型银行开发新转账页面。功能包含金额、收款人和确认的复杂表单。团队完全依赖自动化测试:47个单元测试、12个集成测试、100%分支覆盖。validation.md只包含自动事实。
挑战:发布后收到视障用户投诉:键盘导航时确认模态框丢失焦点,屏幕阅读器不播报验证错误。自动测试通过,但未检查Tab顺序、aria属性、对比度。回归影响移动应用Web视图中12%的用户。回滚造成银行声誉损失和监管机构因违反可访问性要求的罚款。
解决方案:团队为影响金融操作的所有UI功能引入强制性手动事实:
- F-manual-1:在320px宽度和200%缩放比例下,所有交互元素保持可访问且无水平滚动。
- F-manual-2:Tab导航按逻辑顺序经过所有表单字段、确认按钮和"取消"链接,不在模态框中循环。
- F-manual-3:NVDA/VoiceOver屏幕阅读器在字段失焦时正确播报验证错误。
- 对重复检查引入Playwright + axe-core,但为新界面模式保留手动事实。
同时更新plan.md:每个UI组件必须有accessibility-notes.md文件,记录预期焦点和屏幕阅读器行为。
结果:手动事实在下一功能开发阶段发现3个潜在回归。从开发到发布的时间缩短20%,因为更少的bug从QA返回。监管机构可访问性审计无意见通过。团队开始与合规部门共享证据包,加速审批流程。
经验教训: 自动覆盖 ≠ 质量:100%分支覆盖不能保证只有人类才能验证的可访问性
手动事实如果具体且可复现,不比自动事实弱
重复的手动事实是自动化的信号——通过专业工具(Playwright + axe)
plan.md和validation.md的同步更新防止与智能体协作时的"实现漂移"
相关概念: 手动事实
风险矩阵(视觉/UI变更)
人机协同验证
规范、计划和事实的同步更新
标题:与智能体协作:AI原生项目中防止规范漂移
场景:初创公司AgentClinic完全使用Qwen Code生成代码。"智能体详情"功能需要显示每个初始智能体的疾病列表。初始规范描述页面为单体组件。智能体正确实现了它,但一周后再次会话(不同上下文)开始生成不同结构——没有Layout组件。
挑战:智能体在不同会话中对同一文本规范做出不同解释。validation.md中缺少固定事实导致每次新会话都"创造性地"重新理解结构。代码能运行,但架构漂移。审查者花费时间发现规范中未记录的隐性变更。
解决方案:团队引入严格流程:
- 首次实现后更新plan.md:页面 = Layout(Header, Main, Footer)。
- 在validation.md中添加固定结构的事实:
- F-structure:
curl -s http://localhost:3000/agents/1 | grep -E '<(header|main|footer)'→ 恰好3处匹配。 - F-css:
curl -s -w '%{http_code}' http://localhost:3000/static/style.css→ 200。 - F-typecheck:
npm run typecheck→ 0。
- 给智能体的请求现在始终包含:"按照@specs/.../plan.md和@specs/.../validation.md更新实现。未经明确请求不要修改specs文件。"
- 引入验证:每次提交前
git diff --stat main...HEAD+ 给智能体的与validation.md比较请求。
发现需要变更结构时,使用同步请求:同时更新plan.md、validation.md和实现。
结果:架构漂移停止。90%的智能体会话生成与现有结构兼容的代码,无需额外修改。审查时间缩短60%。新开发者(人类)通过validation.md中的事实在几分钟而非几小时内理解项目结构,这些事实作为可执行的文档。
经验教训: 文本规范被智能体不同解释;事实是会话间唯一稳定的契约
"响应包含header/main/footer标记"的事实优于"页面结构正确"——它无需解释即可被机器验证
plan.md + validation.md + 代码的同步更新防止智能体的"创意漂移"
提交前git diff + validation.md的验证创建反馈循环,训练智能体(和人类)遵守契约
相关概念: 事实
validation.md
智能体参与的验证
规范、计划和事实的同步更新
从差异开始
学习建议: 从"逆向"开始练习:拿糟糕的手动检查散文,将其转化为事实——这比阅读理论更能训练"事实思维"
将风险矩阵用作"缺少什么"的检查清单,而非强制集合:目标是发现遗漏,而非制造官僚主义
用真实的curl命令练习:在终端中编写、验证退出代码、复制到validation.md——事实必须是可复现的
为自己的项目创建validation.md模板,用作每个功能的起点——这降低开始工作的门槛
与同事进行"事实审计":一人阅读validation.md,另一人尝试不看代码执行检查——几分钟内发现歧义
与智能体协作时保存验证历史请求——它们成为新会话的培训材料
维护"事实因失败而变更"的日志——明确记录的事实变更是正常的;隐性的则是会作为技术债务回归的反模式
发现重复时从手动事实转向自动事实:三次手动验证的事实是Playwright或集成测试的候选
使用状态生命周期管理利益相关者预期:带解释的"延期"优于发布前才浮现的"被遗忘"事实
即使个人项目也练习形成证据包——为审查者构建论据的技能可迁移到任何团队协作
附加资源: 课程附录c(pr证据包模板):合并请求描述的官方证据包模板——用作所有项目的起点
课程第20部分(事实的隐性变更作为反模式):事实演变和规范技术债务管理的深入分析
Playwright文档(playwright.dev):手动事实自动化工具:截图测试、可访问性验证、移动端视口
Axe-core(deque.com/axe):可访问性程序化验证库——UI手动事实自动化的候选
Hono框架文档(hono.dev):课程示例使用的框架上下文——用于复现练习
Git文档:git diff、git status、git log:功能验证时"从差异开始"实践的基础命令
第9部分自测题(来自原始文档):自检:1) 为什么规范不应是唯一准入条件?2) 事实与愿望有何区别?3) 何时手动验证是事实?4) 如果测试通过但实现不符合requirements.md该怎么办?5) 为什么"规范指导,事实允许"优于"把规范写更好"?
总结:功能验证是一种独立的工作模式,文本规范在此转化为可验证的事实。规范指导开发,但只有事实允许合并。事实是可执行或明确可验证的陈述,而非散文式愿望。四个事实级别(示例、不变量、属性、契约)和风险矩阵帮助为每种功能类型选择最低充分的验证密度。核心工件是带生命周期状态的validation.md,它将意图与证据分离。手动事实如果具体则不比自动事实弱;重复的手动验证是自动化的信号。与智能体协作时事实至关重要:它们克服文本规范在会话间的解释歧义。证据包(evidence bundle)将审查从猜测转化为工件验证。规范、计划和事实的同步更新防止实现漂移,使项目对未来的开发者和智能体都可理解。