仓库
一个没有任何依赖的 Node checkout 服务:totals.js 负责金额计算,store.js 存放商品、五个优惠码和一个空的 redemptions 列表,applyCoupon() 是一个直接抛错的桩函数。已有的两个测试能通过。
预置的优惠码覆盖了通常容易出错的情况:减 20%、减 $10、减 $50(比部分购物车金额还大)、一个已过期、一个已停用。
我们把同一个小型 checkout 仓库交给 Claude Code 六次。三次拿到的是一句话工单,三次拿到的是 spec。结果由一套在任何运行之前就写好的隐藏验收测试评分。模糊提示词大多写出了能用的代码,但没有两次做出同一个产品。
一个没有任何依赖的 Node checkout 服务:totals.js 负责金额计算,store.js 存放商品、五个优惠码和一个空的 redemptions 列表,applyCoupon() 是一个直接抛错的桩函数。已有的两个测试能通过。
预置的优惠码覆盖了通常容易出错的情况:减 20%、减 $10、减 $50(比部分购物车金额还大)、一个已过期、一个已停用。
Claude Code 2.1.284,claude-opus-5-5,无头模式(claude -p),每次都用一份全新的仓库副本,不加载用户设置、hooks 或 MCP 服务器。每次运行耗时 45 到 66 秒,3 或 4 轮。录制于 2026 年 9 月 28 日。
所有文件都在运行包里:fixture、提示词、隐藏验收测试,以及每次运行的 diff 和最终消息。
Add coupon codes to checkout. Make sure invalid codes do not break payment.
Non-goals
- No admin, no stacking, no tax or rounding changes.
- Do not modify src/totals.js or src/processor.js.
Rules
- A coupon can be redeemed once per user. It counts as
redeemed only when an order is paid.
- Applying a second coupon replaces the first.
Acceptance criteria
- AC-2 Unknown, disabled or expired code: applyCoupon
returns { ok: false, reason }. It does not throw.
- AC-4 Redemption is re-checked inside pay.
- AC-5 Amount-off larger than the subtotal floors at 0.
... six ACs in total
测试集只调用仓库里原本就有的函数;拒绝无论以抛出错误还是返回错误结果的形式出现都算通过,所以不会因为模糊运行选了不同的 API 而扣分。标为未写明的检查项,测试的是一句话工单从未提到的规则。
| 检查项 | 类型 | 模糊 | Spec |
|---|---|---|---|
| H1 百分比优惠码作用于税前小计 | 已写明 | 3/3 | 3/3 |
| H2 支付处理方的扣款金额与显示的总价完全一致 | 已写明 | 3/3 | 3/3 |
| H3 过期优惠码不改变总价,付款正常 | 已写明 | 3/3 | 3/3 |
| H4 已停用的优惠码不会被应用 | 已写明 | 3/3 | 3/3 |
| H5 未知优惠码不影响 checkout 付款 | 已写明 | 3/3 | 3/3 |
| H6 同一用户不能兑换同一优惠码两次 | 未写明 | 2/3 | 3/3 |
| H7 一个用户的兑换不影响另一个用户 | 未写明 | 3/3 | 3/3 |
| H8 只应用不付款不会消耗优惠码 | 未写明 | 3/3 | 3/3 |
| H9 第二个优惠码替换第一个,不叠加 | 未写明 | 3/3 | 3/3 |
| H10 减额大于购物车金额时扣款不会低于 0 | 正确性 | 3/3 | 3/3 |
| H11 两个未付款的 checkout 不能同时用掉每人限用一次的优惠码 | 未写明 | 2/3 | 3/3 |
在未改动的仓库上,"拒绝"类检查(H3 到 H6、H10、H11)会直接通过,因为没有任何优惠码会被应用,所以它们只有和 H1、H2 放在一起看才有意义。每次运行都通过了自己写的测试:模糊运行新增 10 到 12 个测试,spec 运行新增 10 或 11 个。
评分表很接近,决策却不是。这些结果是我们直接调用每次运行的代码测出来的,不是读它的总结得出的。
| 决策 | V1 | V2 | V3 | S1 到 S3 |
|---|---|---|---|---|
| 一个用户能用几次优惠码? | 一次 | 不限 | 一次 | 一次(spec) |
| 优惠码在应用和付款之间过期 | 按原价扣款,$54.00 | 拒绝第一次 pay() | 拒绝第一次 pay() | 按折后价 $43.20 扣款 |
| 无效码如何报告 | 抛出 CouponError | 抛错 | 抛错 | 返回 { ok: false, reason } |
| 优惠码匹配 | 不区分大小写 | 不区分大小写 | 不区分大小写 | 精确匹配 |
| 新增公共 API | removeCoupon | removeCoupon | removeCoupon,新文件 coupons.js | 无 |
V1 把按用户记录的 redemptions 表当作提示,实现了每人限用一次。V2 看的是同样的数据,得出的结论是:"优惠码数据里没有按用户或一次性的使用限制,所以不做任何限制。"两者都在最终消息里说明了这一点。只有一个符合业务的本意。
当优惠码在付款前失效时,V1 会悄悄按比顾客看到的更高的金额扣款;V2 和 V3 会拒绝一次付款,前端现在得处理这种情况。每一种都说得通,但没有一种是团队里有人做出的选择。
所有模糊运行遇到无效码都会抛错;spec 运行返回 { ok, reason }。按其中一种写的前端,换到另一种上要么遇到未捕获的异常,要么悄悄忽略错误。工单从没说过前端团队是按哪种契约开发的。
三次 spec 运行都接受了应用之后才过期的优惠码,并按折后总价扣款。spec 要求 pay 重新检查兑换状态,但对过期只字未提,所以 agent 就照做了,三次都是如此。S1 在最终消息里指出了这一点:
Re-checks at pay: pay only re-checks redemption, as AC-4 asks. A coupon that expires or is disabled after being applied is still honoured at payment. If you'd rather reject it there, pay can reuse the same checks applyCoupon runs.
这才是结果的真实样子。spec 不会让 agent 更聪明,它让 agent 的行为可预测,包括它的缺口。这里的缺口在 spec 里一行、在运行总结里一行就能看到,而不是分散在三种不同的实现里。
两个未付款的 checkout,一个用户,一个限用一次的优惠码。两边是否都能拿到折扣,取决于付款时的一次重新检查。以下来自运行 S1 的 src/checkout.js:
export function pay(checkoutId) {
const co = mustGet(checkoutId);
if (co.status !== 'open') throw new Error(`checkout ${checkoutId} is ${co.status}`);
// The coupon may have been redeemed on another order since it was applied here.
if (co.couponCode && isRedeemed(co.userId, co.couponCode)) co.couponCode = null;
const summary = getSummary(checkoutId);
const ch = charge(co.userId, summary.total);
...
if (co.couponCode) db.redemptions.push({ code: co.couponCode, userId: co.userId, orderId: order.id });
V1 和 V3 在没有被要求的情况下写了等价的检查。V2 根本没有按用户的规则,所以也就没什么需要重新检查的。
在一个小而结构清晰的仓库上,当前的模型通常凭一句话就能交付能用的代码。评审要看的是它选了哪些规则:使用限制、异常路径上钱怎么处理,以及错误契约。
消除了大部分差异的是两句话:"每个用户限用一次,以付款为准"和"无效码返回结果,绝不抛错"。即使其他一切都交给 agent,这两条也应该写进 spec。
每次运行都列出了自己的假设。模糊运行的这些假设,正是工单本该包含的评审清单;合并前把它们转成验收标准。
没有 spec 时,Haiku 4.5 在 3 次运行中 3 次都没通过两项优惠码重复使用检查;有 spec 时,它每次都通过了全部 11 项检查。
查看跨模型结果同一个功能做成可直接评审的规格包:任务、验收标准、QA fixture 和回滚方案。
打开优惠码案例九次运行,缺一份文档就决定了移动端 App 会不会崩。
打开 API 运行实录九次针对表结构变更的运行,其中一次删掉了其他服务还在读的列。
打开迁移运行实录本页的每个数字都来自运行包中录制的运行。评分前我们没有修改 agent 的任何输出。