一个 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 当宿主」,而是显式拆成两层:

  1. 规划层(共享契约) — 记录跨仓仍为真的行为:边界协议、跨端旅程、变更提案中的产品/契约部分。它有自己的版本历史与评审,像对待正式代码一样对待契约。
  2. 代码仓(本地实现) — 各仓保留自己的变更与任务:如何在本仓落地、测什么、PR 怎么拆。本地提案引用共享契约,而不是把契约正文复制进来。

消费关系用声明表达,而不是用复制:

  • 代码仓声明「我的规划完全外置到某某规划仓」——命令默认作用在那边;
  • 或保留本地 openspec/,同时只读引用上游规划仓——本地写实现设计,上游提供契约索引。

两种声明解决的是不同问题:前者适合「这个产品的规格真相已完全外置」;后者适合「平台/跨团队契约我只消费、不拥有」。共同点是:契约有主,实现有主,二者用指针相连。

OpenSpec Stores:规划可以自成一仓

OpenSpec Stores(beta) 把上述原则落成了可操作的拓扑启发,而不是又一套目录约定。

核心形状很简单:

team-plans(store:规划仓)
  └── openspec/specs + changes

        │ 按名注册;用 git 共享
┌───────┼────────┐
web-app  api-server  mobile-app

值得记住的不是命令列表,而是三条边界:

  1. Store 就是普通 git 仓 — 提交、推送、PR 评审都由人完成;工具不替你同步。
  2. 声明改变可见性,不偷换作用域store: 指针或 references: 只读引用,改变的是「命令默认看哪里 / 指令里能索引到什么」,不是偷偷把实现任务路由到别的仓。
  3. Workset 是个人工作区,不是共享真相 — 把规划仓与若干代码仓一起打开,方便 Agent 与人同时看见;它不拷贝源码进规划仓,也不定义契约归谁。

因此,一个跨仓促销、登录或结账类变更,合理拆法通常是:共享契约先在规划仓评审通过;各代码仓再开本地 change,任务只描述本仓工作,并引用那份已批准的边界契约。这与「一个 Spec 覆盖前端 + 后端 + 移动端」的直觉一致——覆盖的是边界行为,不是三份实现细节的合订本。

工具选型与光谱背景见 SDD 工具横向对比;OpenSpec 在交付栈中作为规格真相层,见 三层交付栈

对 Lead 与实践者

Tech Lead: 先定「契约仓」是谁、谁有合并权、跨仓 feature 是否必须先合契约 PR——再谈 submodule 目录怎么摆。没有评审边界的同级文件夹,解决不了漂移。

实践者: 接到跨仓需求时,先问检验标准那一句。共享部分进规划层;本仓只写实现 change,并显式引用上游契约版本。不要在前端仓里「顺便」定义 API 语义。

反模式

  • 把实现任务写进共享契约 — 规划层变成三仓任务清单的合订本,评审与归档都无法按仓独立。
  • 共享字段约定只埋在前端仓 — 宿主错位;后端与移动端 Agent 默认读不到,或读到过期副本。
  • 靠 wiki / 口头同步跨仓真相 — 人也许能凑合,Agent 不会;SDD 要的是可版本化、可引用的锚点。
  • 用 workset 代替所有权 — 文件夹一起打开了,不代表契约有主。

先问归谁,再谈放哪

多仓 SDD 里,Spec 放在 submodule 之下还是同级,可以是工程便利问题;契约归谁,才是方法论问题。 共享边界行为给独立规划层,仓内实现留在各代码仓,用声明连接二者——OpenSpec Stores 只是把这件事说清楚了的一种形状。

目录争论可以休止了:先回答「谁拥有这份契约」,路径通常会自己显形。