故事拆分 + Acceptance Criteria + Edge Case
何时触发
- 一个需求看起来 ≥ 1 周工作量
- 写 spec 写到一半发现"还要包括 X / Y / Z"开始膨胀
- "这个 epic 太大,拆一下"
- acceptance criteria 怎么写
- "上线前 edge case 过一遍"
- 团队估算分歧 > 50%(说明大家理解的"做完"不是同一回事)
- PRD review 时被问"那 X 情况怎么办"
何时不触发
- 单文件 hot fix(一行代码改动不需要拆)
- 已经按 phase 切好的 dev 任务 → 用 dev-phase-rollout
- 创意发散阶段(拆故事是收敛阶段,发散用 jtbd-framework / brainstorm)
第一步:INVEST 自检
每个故事过这 6 项:
| 字母 | 含义 | 通过标准 |
|---|---|---|
| Independent | 独立 | 可以脱离其他故事单独 ship 上线 |
| Negotiable | 可商量 | 内容、UI、实现可在 dev 中协商,不是死的 |
| Valuable | 有价值 | 上线后用户 / 业务能感受到具体价值 |
| Estimable | 可估算 | 团队能给出 ≤ 3 天的合理估算 |
| Small | 小 | 1-3 天可完成(≤ 1 个 sprint) |
| Testable | 可测 | 有明确的"做完了"判断标准 |
任何一项不通过 → 必须拆。
第二步:SPIDR 拆分(5 条主路径)
| 路径 | 何时用 | 例子 |
|---|---|---|
| Spike | 技术不确定,先 spike 验证可行性 | "新支付方式接入"先做 1 天技术 spike |
| Path | 一个故事多个用户路径 → 按路径拆 | 注册流程 = 邮箱注册 / 手机注册 / 第三方登录 三个故事 |
| Interface | UI 多端 → 按端拆 | 桌面 / 移动 / API 三个故事 |
| Data | 数据多类型 → 按类型拆 | 单一货币先做 → 多货币扩展 |
| Rules | 业务规则复杂 → 先做主流程,规则后续加 | 主流程不含权限 → 加权限 → 加白名单 |
第三步:7 splitting strategies(备用,SPIDR 不够时用)
- Workflow steps:按工作流步骤拆(注册 → 验证 → 激活 → 完成)
- Business rule variations:先核心规则,再扩展规则
- Major effort:把 80% 工作量集中处先做最简版本
- Simple/complex:MVP 简单版 → 复杂场景版
- Variations in data:先一种数据类型,再扩展
- Data entry methods:先一种输入方式,再扩展
- Defer performance:先功能正确,再性能优化
第四步:垂直切片不水平切片(关键)
❌ 水平切片:DB 层 → Backend API 层 → Frontend 层(每层一个故事)
- 问题:单层完成无价值,必须三层全做完才上线
✅ 垂直切片:完整功能但范围小(一个最小用户场景的 DB+Backend+Frontend 端到端)
- 第一片:单一最简场景全栈打通
- 第二片:扩展到第二个场景
- 第三片:覆盖更多边界
第五步:Acceptance Criteria(Given-When-Then 格式)
Given <初始状态 / 前置条件>
When <用户动作 / 触发事件>
Then <预期结果>
And <附加预期结果>
每个故事至少 3 条 AC,覆盖:
- Happy path(正常流程)
- Sad path(异常 / 错误处理)
- Edge boundary(边界值 / 极限情况)
第六步:Edge Case 4 轴枚举
对每个故事过这 4 个轴,每轴至少列 3 个 case:
轴 1:Input
- 空值 / null / undefined
- 极大值 / 极小值
- 特殊字符(emoji / SQL injection / HTML / 全角半角)
- 错误类型(数字字段输入字符串)
- 多语言(中英日韩 / RTL 阿拉伯)
轴 2:State
- 首次使用(无任何历史)
- 中间状态(部分完成被打断)
- 重复操作(同一动作连点 5 下)
- 多 tab / 多设备同时操作
- 并发冲突
轴 3:Boundary
- 数量边界(0 / 1 / 上限 / 上限+1)
- 时间边界(凌晨跨日 / 月底 / 年底 / 闰年 / 时区切换)
- 容量边界(大文件上传 / 长字符串 / 大列表分页)
- 权限边界(无权限 / 部分权限 / 临时过期)
轴 4:Failure
- 网络断(前端断 / 后端断 / 中途断)
- 服务降级(依赖服务挂)
- 超时(请求超时 / 长任务超时)
- 数据不一致(缓存过期 / DB 主从延迟)
- 非预期数据(脏数据 / 历史遗留格式)
每轴至少列 3 个 case → 4 × 3 = 12 个 edge case 起步。
模板(完整故事)
## Story: <故事标题>
### INVEST 自检
- [x] Independent
- [x] Negotiable
- [x] Valuable
- [x] Estimable(估算 2 天)
- [x] Small
- [x] Testable
### Acceptance Criteria
**AC1(Happy)**:
Given <前置>
When <动作>
Then <结果>
**AC2(Sad)**:
...
**AC3(Edge)**:
...
### Edge Cases
**Input 轴**:
- 空 / null / 长度 0
- 单字符
- 1000 字符(上限)
- 特殊字符 emoji + SQL keyword
- 阿拉伯语 RTL
**State 轴**:
...
**Boundary 轴**:
...
**Failure 轴**:
...
### 拆分历史
- 原 epic: <epic 名>
- 拆分路径: SPIDR-<P/I/D/R> + Workflow steps
- 兄弟故事: <story 2>, <story 3>
Anti-Rationalization
| 逃逸路径 | 为什么不行 |
|---|---|
| "故事太多了 INVEST 走不完" | 走不完说明你应该拆,正是 INVEST 要解决的 |
| "AC 写一条 happy path 够了" | 三条最少:happy / sad / edge。少一条 = 故事不完整定义 = 测试漏洞 |
| "Edge case 太多写不完" | 4 轴 × 3 个 = 12 个起步线。写不完说明你需要再拆一次故事 |
| "水平切片更省事一层一层来" | 水平切片每层完成都没价值,要全部完成才能上线 = 一次性大爆炸 |
| "技术 spike 也算一个故事吧" | Spike 是 INVEST 失败的应急路径,不是常规故事。Spike 必须有时间 box(≤ 1 天)+ 明确产出(学到 X / 排除 Y) |
| "拆这么细 stand-up 时间不够" | 拆细的成本在 PM 端(一次性);不拆细的成本在团队全程(估算偏差 + 测试漏 + 上线返工) |
| "acceptance criteria 测试时再补" | AC 是"做完了"的定义。事后补 = 开发期间没有目标 = 做出来不一定符合期望 |
| "INVEST 太理论了,跳过" | INVEST 不是理论,是预防核武器:没 Independent → 上线被卡 / 没 Valuable → 团队做了没价值的工作 / 没 Testable → 不知道做完没 |
关联
- 拆完前优先级排序:用
rice-prioritization先排 - 拆完后失败推演:用
pre-mortem跑风险 - 拆完后进 sprint:用
product-management:sprint-planning - 写完 spec 后交付实现:确保 spec 自包含 + 按 phase 分阶段推进
Status
v1.0 — 2026-05-08 product-thinking plugin v0.1.0 首发。