一个 feature 同时改 web、api、mobile——Spec 该写在谁的 openspec/ 里?
多仓或 submodule 工作区里,这个争论几乎每周都会冒一次。有人主张塞进「主仓」(通常是后端);有人主张放在与各 submodule 同级的目录,方便一眼看到;还有人让每个仓各写一份,指望靠评审对齐。争论的表面是路径,内核却是:这份规格到底归谁,谁有权改,谁以它为验收锚点。
与 SDD 真相源 一脉相承:当 Agent 生成大部分代码时,稳定的是人写的规格。跨仓场景只是把这句话推得更远——若真相源被拆散或寄居在错误的宿主上,Agent 与人都无法对齐。
一条检验标准:后果是否约束其他仓
先别急着画目录树。对每一份即将写入的 Spec,问一句:
这份规格写明的行为或约束,是否要求其他仓的人也必须遵守?
| 答案 | 归属 | 典型内容 |
|---|---|---|
| 是 | 共享边界契约 | API 字段与错误语义、跨端用户旅程、服务间事件契约 |
| 否 | 本仓实现规格 / 本地变更 | 组件拆分、内部模块边界、本仓任务拆解与实现细节 |
这条标准与 ADR 圈常见的「后果是否外溢」同类:约束外溢到其他服务,就该有中央或独立家园;后果完全本地,就留在服务内。多仓 SDD 里,共享契约需要独立所有权;物理路径只是所有权的投影,不是反过来。
三种物理摆放为何常错位
摆放本身没错,错在用路径假装解决了归属。
| 摆法 | 看起来解决了什么 | 实际常出的问题 |
|---|---|---|
| 塞进某一 submodule | 「总有一个地方放」 | 默默指定宿主仓;其他仓变成二等公民,评审与 Agent 入口都偏斜 |
| 与 submodule 同级目录 | 工作区里一眼可见 | 若没有独立 git 历史与评审边界,仍是「方便的文件夹」,不是可版本化的契约主 |
| 每仓各写一份 | 各仓自治 | 复制漂移;Agent 读到互相冲突的真相源,跨仓 feature 必返工 |
同级目录尤其容易产生错觉:在 IDE 里它和 web/、api/ 并列,看起来像「跨仓 Spec」。但若它只是 monorepo 根下的普通文件夹、没有独立的评审与发布节奏,所有权仍未成立——谁都可以改、谁都不对漂移负责。
两层模型:规划层持契约,代码仓持实现
更稳的默认拓扑不是「找一个 submodule 当宿主」,而是显式拆成两层:
- 规划层(共享契约) — 记录跨仓仍为真的行为:边界协议、跨端旅程、变更提案中的产品/契约部分。它有自己的版本历史与评审,像对待正式代码一样对待契约。
- 代码仓(本地实现) — 各仓保留自己的变更与任务:如何在本仓落地、测什么、PR 怎么拆。本地提案引用共享契约,而不是把契约正文复制进来。
消费关系用声明表达,而不是用复制:
- 代码仓声明「我的规划完全外置到某某规划仓」——命令默认作用在那边;
- 或保留本地
openspec/,同时只读引用上游规划仓——本地写实现设计,上游提供契约索引。
两种声明解决的是不同问题:前者适合「这个产品的规格真相已完全外置」;后者适合「平台/跨团队契约我只消费、不拥有」。共同点是:契约有主,实现有主,二者用指针相连。
OpenSpec Stores:规划可以自成一仓
OpenSpec Stores(beta) 把上述原则落成了可操作的拓扑启发,而不是又一套目录约定。
核心形状很简单:
team-plans(store:规划仓)
└── openspec/specs + changes
▲
│ 按名注册;用 git 共享
┌───────┼────────┐
web-app api-server mobile-app
值得记住的不是命令列表,而是三条边界:
- Store 就是普通 git 仓 — 提交、推送、PR 评审都由人完成;工具不替你同步。
- 声明改变可见性,不偷换作用域 —
store:指针或references:只读引用,改变的是「命令默认看哪里 / 指令里能索引到什么」,不是偷偷把实现任务路由到别的仓。 - Workset 是个人工作区,不是共享真相 — 把规划仓与若干代码仓一起打开,方便 Agent 与人同时看见;它不拷贝源码进规划仓,也不定义契约归谁。
因此,一个跨仓促销、登录或结账类变更,合理拆法通常是:共享契约先在规划仓评审通过;各代码仓再开本地 change,任务只描述本仓工作,并引用那份已批准的边界契约。这与「一个 Spec 覆盖前端 + 后端 + 移动端」的直觉一致——覆盖的是边界行为,不是三份实现细节的合订本。
工具选型与光谱背景见 SDD 工具横向对比;OpenSpec 在交付栈中作为规格真相层,见 三层交付栈。
对 Lead 与实践者
Tech Lead: 先定「契约仓」是谁、谁有合并权、跨仓 feature 是否必须先合契约 PR——再谈 submodule 目录怎么摆。没有评审边界的同级文件夹,解决不了漂移。
实践者: 接到跨仓需求时,先问检验标准那一句。共享部分进规划层;本仓只写实现 change,并显式引用上游契约版本。不要在前端仓里「顺便」定义 API 语义。
反模式
- 把实现任务写进共享契约 — 规划层变成三仓任务清单的合订本,评审与归档都无法按仓独立。
- 共享字段约定只埋在前端仓 — 宿主错位;后端与移动端 Agent 默认读不到,或读到过期副本。
- 靠 wiki / 口头同步跨仓真相 — 人也许能凑合,Agent 不会;SDD 要的是可版本化、可引用的锚点。
- 用 workset 代替所有权 — 文件夹一起打开了,不代表契约有主。
先问归谁,再谈放哪
多仓 SDD 里,Spec 放在 submodule 之下还是同级,可以是工程便利问题;契约归谁,才是方法论问题。 共享边界行为给独立规划层,仓内实现留在各代码仓,用声明连接二者——OpenSpec Stores 只是把这件事说清楚了的一种形状。
目录争论可以休止了:先回答「谁拥有这份契约」,路径通常会自己显形。