实践6|SDD 实战:AI 写的规格被推翻了四次

实践6|SDD 实战:AI 写的规格被推翻了四次

实践4 是 PM 视角:素材是需求文档 + 原型,装 pm-execution 插件,终点是一份可评审的 PRD。当时结尾写了句"AI 出的是初稿不是终稿",也留了个尾巴------PRD 之后呢?

这次把尾巴接上了,但接法不一样。这回是技术经理视角:不装 PM 插件,PRD 由 grill-me 拷问 + to-spec 落规格产出;PRD 定稿后进 OpenSpec,拆成四个 change,一路走到写代码、跑测试、归档。

前后六天(日常工作的管理等事情杂活多,正经八两的只有周六,还是带娃之余,总工作量算 2人天 吧),主线是:

复制代码
摸清现状 → 拷问补洞 → PRD → 拆 change → propose → 人工评审 → apply → archive

这套流程里最关键的动作不是任何一次生成,是让 AI 去读代码、读 DDL,把自己写的规格推翻。六天里 AI 写的规格被推翻了四次,每次都拦下一个会上线的错误:两个提权陷阱、一条断言了不存在的数据库列的验收标准,和一个放错模块的架构决策。

一、为什么这个需求适合 SDD

需求本身很简单:三套 B 端后台要接集团统一 IAM 的单点登录。

但它的形状正好卡在规范驱动开发的靶心上:

需求相同,实现基线各异。 三套后台分属三个独立 git 仓,认证实现是三个代际:一套自研 Interceptor、一套 Spring Security、一套 Shiro + java-jwt。同一句"接 SSO",在三个仓里的落点、改动量、风险点完全不同。

我对三套的熟悉度不一样。 有的仓我改过,有的只是听说过。这种"我自己都不敢说每处都清楚"的场景,如果靠对话式 vibe coding 推进,很容易在第三个系统上把前两个的假设照搬过去------而这三套系统恰恰在关键行为上互相不一致(后面会看到,同一条验收标准在三个系统里的工作量差了一个量级)。

规则可以共享,落地值不能。 "身份怎么解析"、"映射表什么语义"、"IAM 的 token 能不能下发前端"是三端共用的;而"token 放哪"、"会话多长"、"回调路径叫什么"必须各自定。这种天然分层,正是 SDD 里"共享 spec + 系统 spec"的结构。

选型上没纠结:不是 0→1 新项目,是存量多仓改造,所以选了 OpenSpec,而不是那些更适合从空目录开始的工具。

三样东西一次装完:

bash 复制代码
# 需求拷问
npx skills add mattpocock/skills --skill=grill-me
# 出 PRD 等价规格
npx skills add mattpocock/skills --skill=to-spec
# SDD 主体
npm install -g @fission-ai/openspec@latest

# 在仓库根初始化一次(用 Cursor 就 --tools cursor)
openspec init --tools claude

openspec init 会生成 openspec/ 目录和 /opsx:propose/opsx:apply/opsx:archive 这套 slash 命令。装完必须重启会话,否则命令加载不进来------我第一次以为装失败了。

二、前置:从三份现状文档到一份 PRD

2.1 先摸清现状,再谈需求

PRD 之前有一步容易被跳过但省不掉:先把三套系统的认证现状摸清楚,落成文档。

这一步用了多 agent 并行,三个 agent 各读一个仓。这个场景是我第一次觉得多 agent 是"正确解"而不是"能用":三个仓彼此零代码复用,子任务无依赖,产出都是结构化事实(哪个文件、哪一行、什么配置)。

关键做法是先踩点五分钟,再派 agent。 没直接把"分析三套认证"甩出去,而是先跑几轮定位:列各仓模块、按 login|auth|token|sso 捞文件、从 pom 反推技术栈。然后把摸到的确切文件路径写进 prompt,后面跟一份"请回答这 9 个问题"的清单。

agent 拿到的不是"去找找看",而是"这几个文件在这,把这些问题答了"。这是整件事效率差异最大的地方。

三个 agent 耗时差了三倍多(8 / 23 / 29 分钟),跟每套系统认证逻辑的分散程度正相关。时间差反而是好事:最快那份 8 分钟就回来了,可以先过一遍,而不是干等半小时。等待期间我并行确认了三个范围问题,后来验证都问对了------如果猜错范围,得白干一半。

高风险结论必须回头核验。 有个 agent 在"不确定"一节里写:某个 JWT 工具类里有对固定字符串 token 的特殊处理,跳过验证直接返回超管 userId,"但不确定是否在线上环境会产生安全隐患"。

如果直接抄进文档,它就是个"待确认的小问题"。于是让 AI 回去读源码:无条件,没有任何环境判断。然后顺手查了同一个仓里验证码的跳过逻辑------那里有 profile 守卫。

同一个仓、同一批人写的代码,验证码跳过记得加环境判断,万能 token 没加。 这个对比才是把它从"待确认"升级成"P0 必须先修"的依据:不是猜它危险,是同仓的另一处证明了作者知道该加守卫、这里忘了。核验成本三四分钟,换来四条结论从"agent 说的"变成"有人真看过的"。

还有三件事 agent 给不了,得人自己合:三套用户体系完全独立这个结论(每份报告各自说用户表叫什么,三份都对,但"三张表没有外键、没有同步链路、同一个运营人员是三个账号"只在横向对比里浮现,而它是接 SSO 的首要设计问题)、改造成本排序、误报排除(比如某个配置看着像"本系统支持 SSO",实际是本系统调用别人开放接口的出口配置,方向相反)。

一句话:多 agent 省的是"读"的时间,不省"判断"的时间。

2.2 grill-me:让 AI 反过来拷问我

这是跟实践4 最大的分野。实践4 我是"给素材、要 PRD",AI 单方面综合;这次是 AI 一个个问我,我一个个答,最后落一份决策日志。

先看了 skill 定义,grill-me 正文只有一行:

markdown 复制代码
---
name: grill-me
description: A relentless interview to sharpen a plan or design.
disable-model-invocation: true
---

Run a `/grilling` session.

没有流程、没有模板、没有输出格式。真正定义这次拷问形状的是我附在后面的参数:

markdown 复制代码
/grill-me 基于已附文档做需求拷问,不要重复文档里已写明的事实。
上下文文件:三份系统现状文档 + IAM 官方 SSO 技术说明书
主题:3 个存量系统接入同一套 SSO。
规则:
1. 一次只问一个问题,每个问题给推荐答案
2. 只问文档没钉死的业务/架构决策(IdP 最终选型、账号映射规则、单点登出范围、
   token 放置位置、老 session 共存策略、降级登录、未匹配账号处理)
3. 能从上面文件或代码库读到的别问
4. 问完输出决策摘要(Decision Log)

第 2、3 条是关键。"能读到的别问"这句省掉了至少一半废问题。 没有它,AI 大概率会问"你们现在用什么 token 机制""密码怎么加密"------这些三份现状文档里写得比谁都清楚。

"一次只问一个"比一次给四个有效。 AskUserQuestion 一次能塞 4 个问题,但我要求一次一个。结果 13 个问题里至少 4 个是被前面答案改了形状的:

markdown 复制代码
Q1 覆盖范围 → 我答:三端都保留原认证方式,SSO 只是新增一种
              ↑ 这句直接把「老 session 共存策略」从难题降级成默认前提

Q2 映射键   → 我答:手机号
              ↑ 于是 Q5 必须问「匹配不到怎么办」
              ↑ 于是 Q6 必须问「匹配到多条怎么办」(手机号无唯一约束)
              ↑ 如果 Q2 选了 sub 做映射键,Q5/Q6 根本不存在

批量提问会按预设清单问完,不会顺着我的答案往下挖。

我改了 AI 的推荐答案两次,都是它想窄了。 一次是映射键:AI 推荐用 IAM 的 sub 做权威键,技术上更干净(sub 永不变),但它没考虑到其中一套系统的用户表根本没有工号列,用工号认领要先加列,而我不想动那张多端共用的表。另一次是未匹配账号:AI 推荐"直接拒登",我改成"自动建本地账号,零权限",因为零权限账号不带来越权面,但能省掉"登不进去 → 找管理员 → 开号 → 再登"这一整轮。

记一下这个模式:AI 的推荐答案偏向"技术上最规整"和"风险最小",我的修正基本都往"实际落地成本"和"使用体验"方向拉。 两种视角都需要,但它给不了第二种------它不知道加一列要走什么流程,也不知道用户被拒登会打谁的电话。

十三个决策点落成一张表,每条含结论 / 理由 / 未决项。这份决策日志后面被引用了无数次,是整个流程里性价比最高的文档。

2.3 to-spec:模板要改,别硬套

输入三样:决策日志 + IAM 官方文档 + 三份系统现状。这一步明确不重新追问、不读代码、不拆任务、不写代码。

to-spec 自带模板是 7 段英文结构。我要的是 8 段中文,差异在两处:我多要了 AC(验收标准)和 Edge cases(边界情况),砍掉了 Further Notes。

这两段是我加的,不是模板给的,而它们最后成了整份规格里最有用的部分:AC 33 条全部可机器验证,直接就是测试清单;Edge cases 把"IdP 不可达 / 邮箱冲突 / 绑定中途关闭 / 时钟偏移"逼出了明确预期。

参数里下的几个死命令:

markdown 复制代码
4. AC 每条以"ACn"编号开头,每条可机器验证,禁止"流畅""直观"等不可测词
5. User Stories 必须覆盖:终端用户单点登录、单点登出、首次绑定、降级登录、账号冲突
6. Edge cases 必须覆盖:IdP 不可达、email 冲突、绑定中途关闭、时钟偏移
7. 文件头加一行:"> 本文件在需求确认阶段充当 PRD,确认后由 OpenSpec 消费为 proposal 输入"
8. 不写代码、不碰 openspec/ 目录、不重新访谈

第 4 条很有用。不写这句,AC 里一定会出现"登录体验流畅""提示清晰"这类没法验的话。第 5、6 条的"必须覆盖"清单把我担心 AI 漏掉的场景提前钉住了,结果没有一类落空。

最后一条教训跟内容无关,跟编号有关。 这份规格里有两个编号错误,都不是靠眼睛看出来的。一个是纯 typo(AC28 打成 Ace28,一个字符,但 AC 是要拿去当测试清单的)。另一个性质严重得多:验收清单里写的是"零权限账号的提示文案(AC12 对应的用户故事)",grep 核对才发现零权限那条是 AC19,AC12 讲的是 token 过网关。

这个错的位置很坏------它在验收清单里,是要拿去指导人工验收的。如果没核出来,会一路带到设计、带到测试用例。

两条教训,后面几天至少又救了三次:

  1. 带连续编号的产物写完要跑一遍编号连续性检查,别靠肉眼。
  2. 交叉引用要反向核内容,不是核编号存在。 不是看"AC12 存在吗"(永远能通过),而是看"AC12 的内容是不是我想指的那件事"。

三、拆四个 change:解耦的实质不是"拆开"

PRD 定稿,进 OpenSpec。第一个决定是切片方式------别开一个巨型 change。

sql 复制代码
add-central-sso          中央能力:OIDC 交互契约、身份解析规则、映射表语义、
                         登出语义、凭证隔离。archive 后变成 specs/sso-core/spec.md,
                         三个系统共享。不改任何一个子仓的业务代码。
sso-enable-system-app    只改 APP 后台
sso-enable-system-mall   只改商城平台后台
sso-enable-system-comm   只改社区管理端

然后把这个想法连同 PRD 一起丢给 AI,让它教我怎么用 OpenSpec 落这个拆法:

sql 复制代码
我想使用 openspec,拆 4 次 propose 命令;
1. add-central-sso ------ 中央能力:选一些公用的能力和规则,避免大 change
   会导致三个系统改动耦合。
2. sso-enable-system-app ------ 只改 app
3. sso-enable-system-mall ------ 改商城
4. sso-enable-system-comm ------ 改社区

上下文核心是 docs/prd/sso-3sys.spec.md,3 个系统引入中央能力,
以及相关系统的代码说明 md。

帮我看下如何定制 prompt,包含"输出要求"描述等,方便后续 apply

3.1 光拆开并不解耦

这是这天真正改变了拆法的发现。拆四个只是形式,真正的开关在另一处:

change New Capabilities Modified Capabilities
add-central-sso sso-core
sso-enable-system-app sso-app
sso-enable-system-mall sso-mall
sso-enable-system-comm sso-comm

关键在最后一列。三个系统 change 如果把 sso-core 列进 Modified Capabilities,就会各自生成一份 sso-core 的 delta spec,archive 时三份 delta 打到同一份主 spec 上。

这正是我想避免的改动耦合,而且它要到第三次 archive 才会暴露------前两次看起来一切正常。

正确关系是:三个系统的 spec 引用 core 的规则(在 design 里写"遵循 sso-core 的哪几条 Requirement"),而不是修改它。archive 后是四份并列的 spec,sso-core 只有一个写入者。

拆分是形式,写入者唯一才是实质。

3.2 AC 必须在 propose 之前分配完

理由很直接:同一条 AC 落进两个 change,就会得到两份互相漂移的验收标准。

切分依据是"规则 vs 落地值":sso-core 拿 21 条(协议交互、身份解析规则、不变量、凭证隔离),每个系统各拿同一组 12 条(值随系统不同:token 载体、会话时长、菜单接口返回空、验证码与失败计数、登录日志可区分 SSO)。

21 + 12 = 33,当时无重复无遗漏。(这个数字当晚被一轮代码调查改成了 22 + 13 = 35。)

配套一条:core 那些规则的 Scenario 写在 core spec 里,但三个系统的 tasks 里各自要有对应的验证任务。规则集中、验证分散。

3.3 通用规则写进配置,不写进 prompt

这是我问"如何定制 prompt"时得到的答案里最实用的一条。

openspec instructions 会把 openspec/config.yaml 里的 context / rules 注入每个 artifact 的生成指令,而且 propose 技能明确要求"作为约束遵守、不得抄进产物"。所以:

通用规则在 config.yaml 写一次,四个 prompt 只写各自的范围。少写一遍就少一处漂移源。

最后写进去的规则分四组共 18 条,摘几条有代表性的:

  • specs:Requirement 标题用中文并标注来源 AC、Scenario 只描述外部可观察行为(不出现类名方法名调用次数)、禁用不可测词、一条 AC 只能落一处
  • design:必写四件事(接入落点 / 事务边界 / 测试接缝 / "零改动"如何被外部验证)
  • tasks:先测试后实现、每个 task 给可验证点、一个 change 只改一个子仓

一个小坑:config 写完要验证能解析------它错了不会有明显报错,只是规则静默失效。

3.4 两个会静默失败的坑

OpenSpec 的四个产物:proposal(WHY)→ specs(WHAT,行为契约)/ design(HOW)→ tasks(实施清单,- [ ] X.Y 格式才会被 apply 追踪)。tasks 依赖 specs + design 两者。

两个坑不报错,直接静默失效:

  1. Scenario 标题必须正好 4 个 # schema 原文写着"用 3 个井号或用列表会静默失败"------不报错,直接不算 Scenario。
  2. status 只看文件存在性。 先写了 tasks.md 会让 tasks 显示 done,而 specs 其实从没生成过。判断必需集要走依赖边,不能看 status。

还有一个 archive 相关的:新 capability 的 delta spec 必须以 ## Purpose 开头且不少于 50 字符,否则 archive 生成的主 spec 只留一个 TBD ... Update Purpose after archive 占位符等人手填。这属于"当时不管、以后一定要管"那一类------我写 delta 时特意确认过长度(写了 210 字符),所以后来 archive 是干净的。

3.5 执行顺序

bash 复制代码
propose 1 → 评审 → apply 1 → archive 1 → 此时 specs/sso-core/spec.md 才存在
                                            ↓
                                  propose 2 / 3 / 4(可并行,互不影响)

不等 archive 就开 2/3/4 也行------我在四个 prompt 里都写了兜底读取路径 openspec/changes/add-central-sso/specs/sso-core/spec.md

四、propose 的 prompt 结构,和四个文件怎么评审

4.1 prompt 里每次都该写的四段

四个 prompt 都很长,但结构一样。摘第一个(中央能力)的骨架:

diff 复制代码
/opsx:propose

change 名:add-central-sso
capability:新增 sso-core(archive 后成为 openspec/specs/sso-core/spec.md)
Modified Capabilities:空

【必读上下文】(先读完再动手)
- PRD(唯一需求来源,重点 AC、实现决策、测试决策三章)
- 决策日志(D1--D16 决策理由)
- IAM 集成技术说明书(IdP 契约:端点、userinfo 字段、未开放能力)

【本 change 的定位】抽出三套系统共享的 SSO 规则与交付物,使后续三个系统
change 之间零耦合。它不改任何一个子仓的业务代码。

【sso-core 必须承载的内容】共 9 条,其中关键两条:
- 身份解析顺序(已绑定 / 首次绑定 / 冲突拒登)作为三端统一规则
- 零权限账号的统一形态:自动建号时关联一个预置的、不挂任何菜单的默认角色;
  角色 ID 由配置提供,不得硬编码。注意这不是「不关联角色」------代码调查
  已确认零角色在其中两套系统会抛异常/被拒登
(其余:OIDC 交互契约、state 的一次性消费、映射表字段语义、三条不变量、
  登出语义、凭证隔离)

【本 change 承载这 22 条 AC】一条不漏、一条不多:(逐条列出编号)

【明确不在本 change】
- 任何一个系统的接入落点、接口路径、表名、token 载体、会话时长
- 上面 22 条之外的 AC
- PRD 第五章列的 14 条非目标

【tasks.md 的边界】只允许包含三端共享、且不落在任何单一子仓的交付物。
不要在 tasks 里写任何业务代码改动。

【未决项分三档处理】(已在 propose 前完成一轮代码调查,别重复劳动也别推翻结论)
① 已定稿、直接照 spec 执行,不要重新讨论:
   零权限账号 = 预置不挂菜单的默认角色,代码只读配置、不做 get-or-create
② 需要我去问外部团队(不要自己拍板,也不要写成两套并列方案):
   某个回调地址形态能否在 IAM 侧注册。把待确认的问题原文列给我
③ 可以留在 design.md 的 Open Questions(不影响 specs、方案与拆解):
   9 对 client_secret 由谁保管、放哪个配置项、轮换周期

这四段每次都该写:必读上下文 / 承载哪些 AC / 明确不在本 change / 未决项分三档。

最后那段尤其重要------把"已定稿不许推翻"、"要问外部不许拍板"、"可以留成 Open Question"三档分开,AI 就不会把已经查清的事又拿出来讨论一遍,也不会把该问人的事自己拍了。

跑完生成四个文件:

4.2 四个文件各看什么

四个文件权重完全不同。

spec.md------看得最严。 它是行为层面的精确契约(Given/When/Then),也是唯一会进 archive 变成长期真理源的文件。写错了一旦 archive,后面所有开发都对着错的规范跑。重点三件事:

  1. ADDED / MODIFIED / REMOVED 标得对不对------有没有把不该删的老行为标成 REMOVED?有没有漏掉该 ADDED 的新接口?
  2. Scenario 是不是 Given/When/Then 格式------写成散文("用户能登录")就打回重写
  3. Scenario 是否覆盖了分配给本 change 的 AC------少一条就是漏需求

proposal.md------快速过,重点只看 Impact 段。

这一段的实际用途是提前找上游、备数据,不用等到 apply 才发现卡住。

design.md------技术经理重点看。 最该看 Decisions 和 Risks,以及它列出的"需要你去确认"的问题。这次它列了一条要我去问 IAM 团队的,写成了可直接转发的原文:三个编号问题加一句背景说明,我复制就发出去了。"帮我把待确认问题写成可直接转发的原文"是 design 阶段一个被低估的产出。

tasks.md------审结构、粒度、边界,不审代码对不对。 它是 apply 的执行清单,也是我和 agent 之间的"进度合同"。看四件事:每个 Scenario 有没有对应 task、每步是不是原子级、有没有越权到其他 change、有没有把"验证方式"写在 task 里。

实操建议:有问题一般直接在会话里让 AI 改;如果是越权(改了不该它改的仓),自己手动删更快。

4.3 想偷懒不细审的话,至少问三句

三个系统的 propose 可以并行跑,但有几点提醒:

  1. 注意 token 的近 5 小时用量够不够。 我跑完三个 propose 加一个 apply 就见底了,只能等冷却。
  2. propose 后主动问 AI 需不需要提供 DDL,或者直接连 MySQL MCP。tasks 里一般也会包含核实 DDL 的前置任务,提早给反而省 token。
  3. 想偷懒不逐行审,至少问一句你关心的流程在不在。 比如"TDD 流程对不对"、"本次需求里那个核心设计你做了没有"。

第 3 条是有实际战果的------后面 apply 阶段那句"是按 TDD 开发吗",直接逼出 18 个漏写的测试,其中一个当场锁住了一个令牌泄漏隐患。

五、规格被一手事实推翻四次

前面四节都是流程。这一节是六天里真正改变了结论的事。

"让 AI 产出规格"和"让 AI 验证规格"是两个独立动作,不能省第二个。 前者靠推理,后者靠读代码、读 DDL。

5.1 起因是 OpenSpec 自己的一条判据

写完四个 prompt 后,它按 OpenSpec 的 schema 原文重新审了 Open Questions 的准入标准:

Open questions are for genuinely deferrable unknowns, not decisions you skipped. If a question would change the specs, the chosen approach, or the task breakdown, resolve it now - ask the user instead of guessing.

用这条判据回看 PRD 里留的四项未决,三项不合格:

未决项 会改变任务拆解? 判定
自动建号时三端主表非空字段写什么值 不合格,得先查
商城零角色用户能否通过权限加载 不合格,得先查
某回调地址形态能否在 IAM 注册 不合格,但只能问外部
9 对 client_secret 由谁保管 不会 ✅ 这一项才算合格

"留成 Open Question"和"把决定推给下一阶段"是两回事。 后者伪装成前者,而下一阶段没有你现在的上下文。

5.2 第一次推翻:零权限账号的形态错了

派 agent 去查前两项之前,它自己先意识到一件事:如果系统的权限加载要求至少挂一个角色才不炸,那"零权限账号"的正确形态是"预置一个空角色,新用户关联它",角色数是 1 而不是 0。 而 AC 当时写的是"关联的角色数为 0"。

这两种写法对应的建号逻辑和初始化数据完全不同。三个 agent 各查一个子仓:

APP 后台 商城平台后台 社区管理端
零角色能否登录 ✅ 能 ❌ 抛异常 ❌ 拒登
依据 left join + 空集合保护 角色查询是三表 INNER JOIN,查不到主动抛"账号不存在" 用户-角色关联为空即拒,且在密码比对之前
菜单接口零权限时 ✅ 空数组 200 ✅ 空数组 ❌ 抛异常,前端 catch 分支被注释,白屏无提示
有登录日志表 ✅ 有 ❌ 完全没有 ✅ 有(异步写)
密码失败计数 缓存 key ❌ 机制不存在 ✅ 库里有列

"零权限 = 不关联任何角色"这个写进 PRD 的设想,会让三端里两端直接登录失败。

结论改成:三端各预置一个"SSO 默认零权限角色",该角色不关联任何菜单,自动建号时关联它。权限为零由"角色不挂菜单"实现,不是由"不挂角色"实现。

同一轮调查顺带挖出四个会改实现的发现:

同构参照选错了。 PRD 原写"照某个现成的外部 token 换本系统 token 的 Provider 的形状做"。实际它走的是 C 端加载路径,绕过了客户端校验,而且 C 端有个硬编码的合成角色兜底,平台端那条路没有。平台端自动建号零先例。

一个提权陷阱:不能用超管角色当默认角色。 商城的超管角色枚举有个 permNeed=false 属性,会跳过菜单查询直接拿全权限;更狠的是安全工具类对超管直接放行、不看权限点集合。一个本该零权限的账号会变成超管。

两个建号取值与手工建号相反。社区的"是否首次登录"标记必须置"非首次"(手工建号写"是"会触发强制改密,而 SSO 用户没有本地密码可改);"最后登录时间"不可留空(强制改密判断里的日期差值计算会 NPE,而且日期计算在首次登录判断之前执行,所以只改一个躲不过)。照抄现有建号流程会炸。

现状文档本身也会错。 摸现状那天产出的社区文档说权限码全部硬编码是同一个值,实际有 5 个。二手资料要抽样复核,尤其是当你准备基于它做决策的时候。

剩下三件只能人拍板: 商城没有登录日志表(选新建一张,SSO 与密码登录都写------豁免则出问题无法追溯,只写 SSO 则无对照价值);社区菜单接口空结果抛异常(选改这个接口,不改则那条 AC 无法达成);默认角色谁创建(选各端手工预置、ID 配进配置中心,因为代码在生产自动写角色表是过重的权限)。

第二个跟非目标里"不借本期修既有遗留问题"直接冲突,所以加了明确的例外条款:这不是顺手修旧代码,而是零权限账号这一新形态必然触发的缺陷------并严格限定只改该接口的空结果分支,不修同一仓里其它同类写法(那个仓"空结果当异常抛"普遍存在)。

第三个有代价:默认角色要三端 × 三环境手工预置 9 次,漏一次该环境 SSO 全量登录失败。 对冲手段是新增一条 AC:配置指向的角色不可用时必须显式失败且日志可识别配置错误,不得静默降级。

最后规格从 33 条 AC 变 35 条,决策日志从 13 条变 16 条。

5.3 第二次推翻:四条 DDL 换来一个提权陷阱

跑 APP 那个 change 时,我问了一句"需不需要提供什么 DDL"。AI 按价值排序要了四条 SHOW CREATE TABLE,并说清每条要它干什么:

要它干什么
用户主表 关掉一条前置任务:状态/删除标记有无默认值、邮箱可空性、有无唯一约束
登录日志表 某个列是否存在;顺带查消息列有多长(加前缀会不会截断)
角色表 手工预置默认角色要填哪些必填字段
用户-角色关联表 排查"物理表有实体类没有的列"(另一个仓踩过这个坑)

消息列长度那条是它临时想到的------有条决策要往那个列前面加前缀,却从没查过这列有多长。 它自己补了这个漏。

三条推断证实,三条风险直接删。 其中最干净的一条:某条决策原本否掉"复用现有列"的理由是"要改三处 SQL,且赌一个未核实的列存在",DDL 到手发现该列根本不存在,理由从"不划算"升级为"不可行"。顺带查明那个实体类里有五个字段全是无对应列的僵尸字段。

然后是又一个提权陷阱。

角色表无唯一索引,角色标识撞名数据库不拦。顺着这条线查超管判定:框架里硬编码了一个超管标识字符串,而权限装配把角色标识原样放进角色集合。

如果预置"SSO 默认零权限角色"时把标识取成那个字符串,这个角色会让所有角色校验无条件通过------一个本该零权限的角色变成全角色,且没有任何机制会拦住。 同类的还有角色 ID 不能取 1。

跟商城那个 permNeed=false 的坑是同一类错误的第二次出现:零权限角色的取名/取值撞上框架的超管判定。三端各有各的撞法。这类错误不能靠"在另一个系统见过"来防,每端都要单独查。

还有一个默认值方向是反的。数据权限范围列的默认值是"全部数据权限"。现状文档已查明相关切面全仓不存在、行级过滤实际未生效,所以眼下无害。但我还是要求显式给最小值:

依赖"一个坏了的机制"来保证零权限是不可接受的------那个切面一旦被后人补上,所有 SSO 默认角色瞬间获得全部数据权限。

两条 DDL 还暴露了新风险。 手机号列恰为 varchar(11):IAM 若返回带国际码前缀的号码会超长------严格模式报错,宽松模式截断出一个错误的手机号,后者更危险,会产生一个手机号错误的账号。

5.4 第三次推翻:一条 AC 断言了不存在的数据库列

这一次起因很小:只是想确认"某个字段能不能留空"。

第一份 DDL 顺带暴露一个小陷阱:某个主键列注释写着"自增",但 DDL 里根本没有 auto_increment,ID 实际由 MyBatis-Plus 的雪花算法在应用侧生成。DDL 的注释会骗人。

顺着这条还看出一组对照,两个失败模式的性质正好相反:

漏写的列 失败方式
主键 ID 显式报错,当场发现
启用标记 / 删除标记 静默过滤------关联记录躺在表里,菜单查询就是空,不报错

后者才是真正危险的那个。

第二份 DDL 更要紧:两列在库里不存在,而代码在用。 一列在一个 XML select 里,另一列是某条 AC 直接依赖的失败计数列。

它列了三种可能(DDL 是旧快照 / 从别的环境导的 / 手工删了几列),没有自己拍板。我告诉它:DDL 是最新的,那列已经不存在了;那个查询方法先看看有没有在用,应该是没在用,所以没报错。

这个推测是对的。第一列所在的那个 select 是死 SQL------Mapper 接口里没有对应方法声明,全仓零调用,所以引用一个不存在的列从不报错。

但第二列引发了真问题。它顺着实体注解、登录成功路径和 MyBatis-Plus 的字段策略查下去,结论是:登录成功时那条 update 会拼进一个不存在的列,必然 Unknown column。再查 git,那个"限制 5 次登录失败禁用账户"的提交只改了 3 个 Java 文件,零 SQL 迁移。

两个推论:

  1. 这个安全机制自上线起从未生效。 计数恒为 null,阈值判断永不触发。
  2. 两列同样缺失,只有一列引发故障。 差别就在死代码引用不报错、活路径引用会报错。

没验证的部分它如实标在 design 里:线上实际现象未经运行时确认,异常可能被上层 catch 吞掉。"代码路径上必然抛"和"用户登不进去"之间还隔着异常处理链。

这条 AC 怎么改的,才是这一节的重点。

原写法断言"那两列的值不变",前提被证伪,测试根本写不出来。改成断言这个机制唯一能从外部观察到的后果:连续失败若干次之后,账号仍然启用、仍能用密码登录、再发起一次正确的 SSO 仍能成功。

这样写的好处是:将来缺陷修好(列补上、计数真正生效),这条 Requirement 依然成立、依然有效,不用跟着改。

好的断言写外部可观察后果,不写内部状态。 这是判断一条验收标准写得好不好的实用标尺。

另一条相关 AC 也一并改了。它原来把"失败计数逐次累加、达阈值停用"当回归基线,而那个基线本身不存在。改成判据是"与上线前一致"而非"符合设计预期",并写明该路径有既有缺陷,本 change 的义务是不让它变糟,不是修好它。

顺带记一个坑:补列不是补个列那么简单。补上之后那个机制立刻生效,而它的逻辑是直接永久禁用账号,不是临时锁定。上线即生效,任何用户连错几次密码就得找管理员解封。这是行为变更,需产品确认,所以建议另行立项而不是混进 SSO。

5.5 一个文档习惯:"假设"和"事实"要分开写

我说"暂时无法确认 IAM 支持某种回调地址形态,先按支持进行设计"。它原本只是把 Risks 段那句话的措辞软化了一下。这里不对------这是个决策,不是个风险。风险是"可能出问题的事",决策是"我们选了什么、为什么、错了怎么办"。

所以新增了一条决策,其中最关键的是明写一句"这是一个未获确认的假设,不是已确认的事实"------评审时看到的是"我们选了个默认值并接受其风险",不是"这事已经清楚了"。半年后回看,这一句能省掉一轮重新调查。

配套产出是一张三行判定表,因为答复不是二值的:支持且如预期(无动作)/ 支持但行为与预期不同(改一处取值位置)/ 不支持(改回调页形态,后端零改动)。中间那档最可能发生,也最容易漏。写进表里,将来遇到时是查表而不是重新调查。

前三次推翻都是 AI 自己读代码、读 DDL 查出来的。第四次不一样,它来自我的一句话,而且推翻的是 design 里一条 AI 已经论证过的决策,在下一节。

六、apply:第一次让 AI 写业务代码,被纠正两次

前面几天都在写文档与规格。商城那个 change 是第一次真正让 AI 写业务代码。第一次就撞上两个典型失败:放错模块、跳过 TDD。这两个失败的形状,比这次写的代码本身有价值。

进 apply 之前建议切到 Accept Edits 模式,然后它会按 tasks.md 逐条执行:

6.1 第四次推翻:IAM 客户端放错了模块

AI 做错的事: 把 IAM 的 OIDC 客户端直接写进了认证中心。理由是"SSO 属于认证,认证中心自然放认证的东西"。更糟的是,design.md 里它甚至为此写了一条决策,把错误决策论证了一遍。

我的纠正:

connector,第三方服务都在这个中心,而这个 IAM 之前并没有创建过 client

判定模块归属的依据不是"这个功能属于哪个业务",而是"谁在跟谁说话"。 "调 IAM 的 HTTP 接口"这件事的对话对象是第三方,所以归防腐层;认证中心只是消费方。

而且证据一直在仓里:那个防腐层模块的 README 第一行就写了"第三方接口变动后,只需要修改本中心并重新部署"。它跳过了这一行,直接去看代码结构。

搬迁带来一个谁都没预料到的收益。 原设计里 Provider 要连调两次(用 code 换 access_token,再用 access_token 取 userinfo),IAM 的 access_token 会流经业务层。搬到防腐层后它把两步合成一个 API 出口。结果是:

IAM 的 access_token / id_token / refresh_token 完全不跨出防腐层,业务层无从接触,也就无从下发前端。

这把"IAM token 不得下发前端"那条 AC 从"靠代码审查保证"变成了"靠模块边界保证"。正确的分层顺手解决了一个安全约束。

顺带两个靠读现有代码避开的选型坑,共同点都是"静默失败" :它第一版给 DTO 加了 Jackson 的 @JsonProperty,但仓里统一用 fastjson2,它不认 Jackson 注解 → 下划线字段会静默为 null;MyBatis 的 JSON 列先用了通用 TypeHandler,但仓里既有写法用的是 fastjson2 那个。

架构纠错之后要回头扫一遍 tasks / design 里的落点描述。 这次至少三处漏改:测试替身该放哪、自签证书导进哪个环境、配置键名前缀。纠正一个决定,会让一批依赖该决定的描述同时失效------纠错当时只改了代码,没扫文档。

替身落点那条还引出一条通用推论:替身该放在"真正发起该调用的那一层"。 认证中心搬迁后只跨 Feign 边界调用、自己不发任何第三方 HTTP 请求,在那里起 HTTP 替身等于替一个该模块根本不会发起的请求。

6.2 我问的第二句:是按 TDD 开发吗

事实是tasks.md 是 AI 自己写的,顺序明明白白:先做 stub 与数据构造器 → 先写失败的集成测试 → 再实现,"验证点:上一步的三个测试转绿"。

它实际做的:跳过前两组,从实现直接开写,新增测试文件 0 个。

它为什么会这样(这段是关键): 会话开头它发现配置中心不通、Spring 上下文起不来,于是问我"这次 apply 做到哪一步",我选了"只写代码与 DDL 脚本,基础设施留给我"。

它把这个答复擅自扩大解释成了"不写测试"。 但我说的是基础设施动作(建库、配置中心、证书)我来做,从没说不写测试。

关键区分:测试源码 ≠ 测试执行。 即便集成测试当下跑不起来,也有一大类测试当场就能跑:纯函数(URL 拼装、编码)、mock 掉外部依赖的分支判定。漏掉的正好是这一类------本来就跑得起来的那部分。

补的 18 个测试当场就证明了自己的价值。 其中一条锁住了一处读代码看不出来的隐患:动态令牌是照仓里现成模式走参数 map 传递,再在公共请求头方法里 remove 出来转成请求头。如果哪天那个 remove 被误删、或基类改成先拼 query string 再调公共头方法,令牌就会出现在 URL 里------进 access log、进 nginx 日志。

这正是"IAM token 不得泄漏"要防的事,而且只能靠测试锁住:

java 复制代码
assertFalse(paramMap.containsKey(IamHttpClient.PARAM_ACCESS_TOKEN),
    "令牌必须从参数中移除,否则会作为查询参数出现在URL里,进而落入日志");

所以 TDD 在这里不是流程洁癖------跳过它就漏掉这条断言。

6.3 "需要容器"要拆开问

compact 之后我让它把需要容器的集成测试源码也写出来。结果最大的收获是:"需要容器"这个前提有一大半是假的。

本机 Maven 仓库没有 WireMock,引依赖又要连外网。换成 JDK 自带的 com.sun.net.httpserver.HttpServer,零依赖,当场就跑起来了。 先验了一下沙箱能不能绑本地端口,能绑,于是真实 HTTP 层的验证完全不需要数据库 / Redis / 配置中心:

之前 之后
可实测运行的测试 18 38
其中走真实 HTTP 0 20

「需要容器」要拆成"需要数据库"、"需要 Redis"、"需要配置中心"、"需要真第三方"四件事分别问。 判据是被测对象依赖的是持久化状态,还是仅仅一个 HTTP 对端------后者自己起一个就行。这次因为没拆,一半能跑的测试被推迟了一整轮。

6.4 补测又抓出两个真缺陷

缺陷一:OAuth 的 4xx 被当成传输故障(只有真 HTTP 能抓)。

fake IdP 的集成测试第一次跑,12 个里 3 个红。红的是代码不是测试。

基类的两个方法对错误的处理不一致:get 在配置缺省时内部固定构造一个静默配置,非 200 返回响应体;postJson 传 null,非 200 直接抛异常。

而 OAuth 的 invalid_client / invalid_grant 是带 HTTP 400 的正常业务回调,报文体里才有 error 字段。我原来的换 token 方法没传配置 → 走抛异常分支 → 被上层 catch 成"IAM 不可达"。

后果正好毁掉 AC 明确要求的那条区分:

  • "凭证无效" = 配置错,不该重试,要人去改配置
  • "IAM 不可达" = 瞬时故障,可重试

两者判错,排查方向整体走偏。修法是显式传静默配置:

java 复制代码
return iamHttpClient.postJson(config.getUrls().getAccessTokenUrl(), paramMap,
    IamAccessTokenRespDto.class,
    // OAuth 的 invalid_client / invalid_grant 是带 4xx 的正常业务回调,
    // 按默认(非200即抛)会与「IAM不可达」混为一谈
    HttpConfig.of().isQuiet(Boolean.TRUE).build());

为什么 9 个已有单测发现不了:它们 mock 掉了 adapter,把返回值直接喂进上层,永远走不到状态码判定那一步。这条缺陷只在真实 HTTP 层暴露。

mock 的层次决定测试能发现什么。替身要放在真正发起调用的那一层。 mock 得太高,等于把要验的那段逻辑一起 mock 掉了。

缺陷二:新写的 HTTP client 从来没注入配置(读代码发现)。

写测试期间顺手比对了同目录的 5 个兄弟 client:

复制代码
tsp/TspHttpClient              setHttpConfigVo → 1
hupun/HupunHttpClient          setHttpConfigVo → 1
xcpayment/XcPaymentHttpClient  setHttpConfigVo → 1
d1/D1HttpClient                setHttpConfigVo → 1
bigdata/BigdataHttpClient      setHttpConfigVo → 1
iam/IamHttpClient              setHttpConfigVo → 0   ← 我写的

5 个兄弟都在构造器里注入配置,只有它新写的那个漏了。运行时返回 null,第一次调用就 NPE。 已有单测抓不到,因为它们 mock 了那个 getter 的返回值------mock 把"这个方法本来会返回 null"这件事掩盖了。

新增一个"一族同类实现"里的成员时,把同族全部列出来逐项比对。 这类漏写不是逻辑错,是遗漏模式,靠读自己的代码看不出来(自己的代码自洽),只能靠横向对比。这次是 grep -c 一行命令的事。

6.5 三个流程习惯

判断一个失败是不是自己造成的,先 git stash -u 这次三处既有失败都是这么确认的:Redis 相关的 bean 定义冲突、本地某个 jar 过期、某个第三方配置绑定失败导致 26 个测试红。代价几十秒,收益是不会把既有问题当成新代码的 bug 去改,也不会反过来把新引入的 bug 当成既有问题放过。

规范要落文档而不是 memory。 我提的要求是:"代码规范之前也写了一些在 CLAUDE.md......不建议用 memory"。理由很清楚:memory 只在单个 AI 会话里生效,文档对团队和跨会话都生效。 于是新增了一份模块归属规范,核心是一个三问定位法:

复制代码
这段代码是在跟「本系统之外的东西」说话吗?
├─ 是,对方是第三方系统  → 防腐层(出站)
├─ 是,对方调进来        → 开放平台模块(入站)
└─ 否 → 按业务归属放各中心

更一般的版本:把规则放在工具会主动喂给你的地方,而不是放在需要你记得去看的地方。 反例就在同一天:apply 第一步我要求"切换开发分支再写",它去查发现根本没有仓可以建分支,工作区根目录不是 git 仓库,而那个 change 的 15 条 task 全部写在不受版本控制的目录下。有意思的是,规范文档自己第 6 行就写着这件事,但要求建分支的时候两边谁都没想起来。所以后来把规则固化进了 openspec/config.yaml 的 apply 守则:每次 /opsx:apply 时 CLI 会把它作为操作指引返回,AI 必然看到。

交接文档要写"哪些红不是你的问题"。 最后它输出了一份人工执行清单,每项写清"在哪执行、执行什么、怎么验证做对了",刻意做成单一入口。其中最值得记的是第 0 节列的既有失败:跑全量测试时有 26 个红,跟本次改动无关(git stash -u 验证过)。只写"怎么做"不够------接手的人跑出 26 个红会先怀疑新代码。

清单里还有一处它明确升级为 Open Question、交给我拍板的:配置键名前缀该用三端共用的(运维视角:9 处写入核对,键名一致降低漏配概率)还是仓内既有的(模块归属视角:与仓里既有 6 个第三方配置前缀一致)?两边都成立,所以它没自己拍。 清单里给了三个选项、标注它倾向哪个及理由,并提醒另两个系统会遇到同一问题,建议一次定完。

七、archive:spec 从"计划"变成"法律"

四个 change 里只有中央能力那个走完了 propose 到 archive 的全流程。它产出全是文档和共享约定,不含业务代码,所以 apply 完直接 archive 了。

两个动作的区别是:

apply 是"把 spec 变成代码",archive 是"把 spec 变成法律"。没 apply 之前 spec 只是计划,没 archive 之后 spec 只是历史。

前几天一直在说"archive 后 delta 变成主 spec"。这次真跑完:跑之前 openspec/specs/ 是空目录,validate --all 只有 change 一项;跑之后多出 spec/sso-core,它现在是一等 spec,不再是某个 change 的 delta:

bash 复制代码
=== 跑之前 ===          === 跑之后 ===
✓ change/add-central-sso   ✓ spec/sso-core
Totals: 1 passed           Totals: 1 passed   ← change 已移出 changes/

合并动作本身很简单(纯 ADDED、主 spec 不存在):剥掉 delta 操作头,保留 ## Purpose,21 个 Requirement 挪到 ## Requirements 下。

但验证不能只看它声称改了什么。 skill 明确要求"对每一个有 delta spec 的 capability 重跑比对,不要只比对 sync 报告说它碰过的那些"。我写脚本逐条比对:21 个 requirement 缺 0 多 0、35 个 Scenario 差异 0、AC 集合差异 0。

下游 change 的验证也在这一步落地。三个系统 change 的 specs/ 下各自只有自己那一个 capability,Modified Capabilities 段保留标题写"(无)"并说明 core 是只读上游。前面那句"拆分是形式,写入者唯一才是实质",到这一步才算真的成立。

两个当初做对了、现在才看到回报的细节:一是那 210 字符的 ## Purpose,所以 archive 是干净的、不用回头补 TBD 占位符;二是当初把"落点"排除在 core spec 之外------那个未确认的回调地址假设如果翻车,共享 spec 一字不用改,回退只动一个 change。假设翻车时,翻的范围是可控的。

八、六天的状态盘点

老实说,这不是一个"全部跑完"的故事:

change 状态
add-central-sso(中央能力) propose → apply → archive 全部完成,共享 spec 已落地
sso-enable-system-mall(商城) propose + apply 完成(38 个测试实测全绿,2 个真缺陷已修),待环境验证后 archive
sso-enable-system-app(APP) propose 四件套齐备,apply 未跑
sso-enable-system-comm(社区) propose 四件套齐备,apply 未跑

只做了商城那一个 apply,另两个暂时没做------形状差不多,没必要等全部跑完再写,因为这套流程该暴露的问题已经全暴露了。

还有几件悬着的,如实记一下:外部那个回调地址形态的答复仍未到手(但已退化成一个配置项取值,不阻塞);配置键名前缀的三选一还没拍板,三个系统一起定;那个从未生效的失败计数机制要单独开工单;各环境的 DDL 未逐一核实(本次字段取值全部依据测试环境,推广前要重跑比对);有一条 AC 可能在社区系统上本来就不成立(登出后 token 立即失效),tasks 里写明届时停下来报告,不自行改登出逻辑。

顺带一条实操:注意 token 的近 5 小时用量。 我跑完三个 propose 加一个 apply 就见底了,只能冷却 5 小时。跨系统的大需求要按这个节奏排。

九、这次学到的

按重要程度排。

1. "让 AI 产出规格"和"让 AI 验证规格"是两个独立动作,不能省第二个。 前者靠推理,后者靠读代码、读 DDL。六天里 AI 写的规格被推翻四次,全是第二个动作的功劳。最极端的一次,AI 写的 AC 断言了一个不存在的数据库列,而且连续两天没人发现。

2. 动手前那一轮调查,价值高于之后所有 propose。 它推翻了一个会让两套系统直接登录失败的设计,还挖出两个提权陷阱。如果直接跑 propose,这些会变成 apply 阶段的返工,或者上线后的事故。

3. DDL 这种一手事实,问一次的收益远大于推断十次。 四条 SHOW CREATE TABLE 换来:3 条风险消解、2 条新风险、1 个提权陷阱、1 个新决策。顺带还暴露了我自己漏问的一件事(要往某列加前缀,却没查过这列多长)。

4. 解耦的开关不是"拆几个 change",是"写入者唯一"。 拆四个但都改同一份主 spec,等于没拆,而且要到第三次 archive 才暴露。

5. 通用规则写进工具的配置,别写进 prompt。 config.yamlcontext/rules 会被自动注入每次生成。更一般的版本:把规则放在工具会主动喂给你的地方,而不是放在需要你记得去看的地方。

6. "留成 Open Question"和"把决定推给下一阶段"是两回事。 OpenSpec 自己写了判据:会改变 specs / 方案 / 任务拆解的问题,现在就得解决。 用这条一筛,四项未决里三项不合格。

7. 好的断言写外部可观察后果,不写内部状态。 改后的那条 AC 在缺陷修复后依然成立------这是判断一条验收标准写得好不好的实用标尺。

8. 同一类错误在三个系统里有三种撞法。 零权限角色的取名撞上框架超管判定:商城是超管枚举的 permNeed=false 跳过菜单查询,APP 是角色标识取成 "admin" 让角色校验无条件通过。这类错误不能靠"在另一个系统见过"来防,每端都要单独查。

9. "机制已经坏了所以无害"不是可以依赖的前提。 数据权限默认值是"全部数据",眼下因相关切面不存在而无害------但那个切面一旦被补上就是批量提权。显式写最小值,不吃"坏了"这个红利。

10. mock 的层次决定测试能发现什么。 mock 掉 adapter 的 9 个单测跑得再绿,也发现不了"OAuth 4xx 被当成传输故障"------那段逻辑正好在被 mock 掉的边界后面。替身要放在真正发起调用的那一层。 相关的一条:"环境不通"不能当成"不写测试"的理由,测试源码与测试执行是两件事,而且"需要容器"要拆成需要数据库 / Redis / 配置中心 / 真第三方四件事分别问。

11. 模块归属的判定依据是"谁在跟谁说话",不是"属于哪个业务"。 IAM 直觉上属认证,实际属第三方对接。AI 不但放错了,还在 design 里把错误决策论证了一遍------AI 会为自己的错误选择编出合理理由,这比单纯写错更危险。 相关的两条:仓库里已有的 README 要读(那个模块 README 第一行就写了职责,它跳过了);架构纠错之后要回头扫一遍 tasks / design 里的落点描述(这次至少三处漏改,纠正一个决定会让一批依赖它的描述同时失效)。

12. 机器可查的错误就别用眼睛查。 带连续编号的产物(AC、决策点、章节号)写完跑一遍连续性检查;交叉引用要核内容不核编号存在。另外两条同类的:新增"一族同类实现"的成员时把同族全列出来逐项比对(漏注入配置是纯遗漏模式,读自己的代码看不出来,横向 grep -c 一行就现形);判断一个失败是不是自己造成的,先 git stash -u

十、跟实践4 比,这次差在哪

实践4 结尾我写"AI 出的是初稿,不是终稿",还写了句"也许后续要创建 skill 了"。这次算是回答了那两句。

PRD 的产出方式变了。 实践4 是装 PM 插件、给素材、要 PRD,AI 单方面综合。这次是 grill-me 让 AI 反过来问我 13 轮,我答,落决策日志,再用 to-spec 出规格。后者的 PRD 质量明显更高,因为决策是我做的,AI 只负责逼我把它说清楚。

而且这次的角色不是 PM,是技术经理:PRD 里带了实现决策、测试接缝、映射表语义这些 PM 不会写的东西,而它们恰好是下游 SDD 能直接消费的部分。

多了 PRD 之后的那一整段。 实践4 停在"可评审的 PRD",这次走完了 propose / 评审 / apply / archive。走完之后可以回答实践4 留的那个问题了:PRD 该写多细?

答案是:被真正消费得最多的是 AC 和实现决策两段,而它们恰恰是 to-spec 模板里没有、我自己加进去的。 用户故事那 30 条在 propose 阶段基本没被引用;AC 35 条几乎每一条都被分配、被追溯、被写成 Scenario。

但也多了一层实践4 没有的风险。 实践4 的产物是文档,错了改文档。这次的产物一路到代码,而 spec 一旦 archive 就是长期真理源------写错了一旦 archive,后面所有开发都对着错的规范跑。 所以四个文件里 spec 要看得最严,其它三个可以快速过。

至于 skill:这次没自己写 skill,用的是社区的 grill-me / to-spec 加 OpenSpec。事后看,真正该自己维护的不是 skill,是 openspec/config.yaml------那 18 条规则加上 apply 守则,是我这个仓、这个团队的东西,别人的 skill 给不了。


AI 写的 PRD,被 AI 自己的调查推翻了四次。 每次推翻都省掉一轮返工或一次事故。所以这套流程的价值不在"AI 写得快",在于它把"写"和"验"拆成了两个可以分别下令的动作。第二个动作,是我以前从来没想过要单独下令的。

相关推荐
篮框坏了1 小时前
"DeepSeek Harness 实测:一毛钱干三活,但默认配置是个坑"
后端·ai编程
jobBridge211 小时前
大模型到底是怎么"想"的?我把 Transformer 拆开,发现它其实是个"接词狂魔"
人工智能·后端·编程语言
唐青枫2 小时前
看懂内存地址之后,才算真正入门 Zig:指针、切片与实战
后端
朋克洛德的码农2 小时前
Go并发-sync包四剑客:Mutex、RWMutex、WaitGroup、Once-从入门到原理
开发语言·后端·golang
fulton2 小时前
为什么不用现成的开源工具?NovelOps与6大AI写作工具横向对比
后端
fulton2 小时前
3个月踩坑实录:从想法到270章规划,AI写长篇到底要花多少成本?
后端
fulton2 小时前
为什么AI写到20章就开始"复制粘贴"自己?创意扰动机制详解
后端
艺艺生辉2 小时前
从if-else到策略模式
后端·设计模式
fulton2 小时前
AI写小说失败的第一原因是什么
后端