ADR 0200 — 模块间合约成为 Arquiteto 的版本化工件,dev agent 读取它而不是别人的 worktree
状态
Accepted. 2026-10-01(AT-276,故事 HS-071,史诗 EP-030,第 36 轮;
负责人 10月1日的决定:"o contrato entre módulos vira artefato versionado
do Arquiteto (no molde de module_map/c4_diagram), lido pelos devs pela
ferramenta de leitura; mudar contrato é nova versão do Arquiteto")。扩展了
ADR 0065、
ADR 0068 和
ADR 0131 的无表工件设计;在
ADR 0060 受约束的读取之下,把“写入的 agent 必须能够读取”(RN-164/165)这一规则应用到 dev agent。
Context
症状
2026-09-29 的真实使用(分析 analise-uso-real.md,A30 项,6(e)):
dev-board-engine,seq 1157/1230/1268/1344/1486——五个轮次在其他模块的 worktree 中读取接口。对照 AT-237 的测量
(medicao-do-jev.md),这个 agent 在整轮中有 24 个带工具的步骤:24 步中有 5 步,约占它步骤的 ~21%,是在探索别人的接口。该项目有七个 dev 模块
(piece-catalog、retro-renderer、input-keyboard、game-session、
scoring、persistence、board-engine)。
这里无法测量的: 这五个步骤的 token 成本。9月29日的 event log 存在于维护者的本地实例中(compose
brabo-dev),不在本环境;上面的数字来自分析以及 Jev 测量中的 agent 表,而不是重新执行的查询。成本上的收益可以之后用 AT-239 的同一把尺子(按参与者的 token_usage)测量。
产品给了 dev 什么(在 dev 上测量,2026-10-01)
module_map(domain/architecture/module-graph.ts)是{name, stack, responsibility, dependsOn[]}。它说明谁依赖谁,却对一个模块提供什么只字不提。Arquiteto 的工件 (module_map、project_image、module_routing、c4_diagram)都不携带接口。- dev 的上下文(
Engine.Dev.ContextBuilder)带来 story、task、业务规则以及按模块过滤的 ADR——没有任何邻居的接口。 - dev 的工具(
Engine.Dev.Tools)是read_file、search_workspace、write_file、terminal、rag_search/rag_feedback以及终止类工具:没有任何读取项目工件的工具。看到邻居接口的唯一方式是用terminal读取另一个 dev 的 worktree——一个尚未到达dev的分支上进行中的代码,并且可能在读取之后改变。
Decision
- 合约是 Arquiteto 的工件
artifact.module_contracts。 无表:事件就是记录,按 payload 中的version版本化,当前值是version最大者,平局按seq决定——与GetC4DiagramUseCase/GetModuleRoutingUseCase相同的归约。每次声明携带完整列表并取代之前的;修改合约就是 Arquiteto 发出的新版本,之前的版本继续留在日志中。 - 格式是能解决问题的最小格式:按模块,只写它暴露的东西。
{modulo, expoe: [{tipo, assinatura, descricao}]},tipo取funcao | rota | evento | dado,assinatura必填(最多 300 个字符),descricao可选,每个模块 1 到 40 项。- 它消费什么不写进合约。 在读取时从当前
module_map的dependsOn推导——与 C4 的 Container 层级相同的论据(ADR 0068):地图已经验证了这些边无环,而手写的第二份列表会在第一次修订地图时就与之分歧。“模块 Y 使用 X 的哪些项”这一粒度被排除在外:Arquiteto 在实现之前并不知道,而消费 X 的人会读取 X 暴露的完整列表。 tipo是枚举,assinatura是自由文本。 类型是会改变使用该项方式的东西;签名无法放进单一 schema,因为每个模块的技术栈是自由的(一个 Elixir 函数和一条 Go 路由)。- 上限的存在是因为合约会进入每个 dev 的上下文。 一个变成整个 API 文档的合约,会让每一步都付出读取 worktree 在一步中付出的代价。
- 它消费什么不写进合约。 在读取时从当前
- 与
module_map分开的工件,绝不是它多出的一个字段。 地图是一张表,被整个执行过程读取(task 的 claim、路由、C4),修订它会重新打开环检测。合约以另一种节奏变化——接口在 dev 工作时被发现——修订其中一个不能重新发出另一个,也不能与另一条战线争夺它的 schema(第 36 轮的资源 lane 会给module_map增加字段)。 - Arquiteto 获得
declare_module_contracts(:direct,像route_modules_to_infra:架构声明,无外部效果,绝不是proposed_action)。api 验证并以完整原因拒绝,原因回到模型(RN-061):地图中不存在的模块(列出有效的)、没有module_map、重复的模块、expoe为空或超过上限、未知的tipo、assinatura为空或过长。它的 kickoff 增加第 5 步;该工具进入Engine.Harness.IdiomaDaResposta.ferramentas_de_artefato/0(RN-623)。 - dev agent 获得
listar_contratos_de_modulos(:direct,在Engine.Dev.Tools的注册表中,不在全局中)。受约束的读取:通过路由路径把作用域封闭在项目中(GET internal/projects/:projectId/module-contracts),没有任何参数,dev 的模块来自循环的上下文(ctx.module,由DevAgentServer放入),绝不来自模型。知道模块时,它只完整显示自己的模块以及它所消费的模块;消费它的模块和其他模块以名称出现。文本中最多 120 项,并说明被省略的部分。已离开地图的模块的合约会被说明,绝不被归属。 - dev 的 kickoff 说明合约存在以及在哪里读取它,并说明另一个模块的接口不在其 worktree 中读取。当所需模块没有合约时,工具说明该怎么做:按照 story 和 task 所说的实现,如果 task 依赖一个不存在的接口,就
report_blocked,点名模块和所缺的东西——让 Arquiteto 去声明。它不会让人重读 worktree。
Consequences
- dev 以前花在探索接口上的步骤变成一次固定成本的读取,而他读到的是 Arquiteto 固定下来的版本,而不是另一个 dev 进行中的代码。
- 接口开始有主人和版本。 修改它的是 Arquiteto,以一个新版本;需要变更的 dev 不在邻居的代码中去做,而是阻塞并点名所缺的东西。
- 这不是锁。 没有什么阻止
terminal读取另一个 worktree:工具和 kickoff 改变的是 dev 所知道的路径,并不禁止另一条。关闭对别人 worktree 的读取将是路径约束(ADR 0055),另一条战线。 - Arquiteto 在 kickoff 中多了一步,在同样的 14 次迭代上限之下。在大型架构中它能否与其他七步一起放得下,未测量;耗尽上限仍然会被叙述(
toolloop.limit_reached,RN-166)。 - 没有合约的项目继续工作:读取返回地图,每个模块
expoe: null并附带提醒。没有迁移,没有表。 - 工件的
docs/文件夹(ADR 0148)获得该类型作为版本化类型:当前值覆盖同一个文件,与其他四个一样。 - 不在此处,已声明: 专属界面(合约出现在时间线和
docs/文件夹中,而不在 Arquitetura 标签页中);对 Jev 收益的影响(AT-239 按步骤测量工具;这一改动改变了 dev 的菜单,而scripts/jev/catalogo.json是 9月29日的快照,此处未重新生成);ExUnit 测试是在mix之外运行的(本环境中repo.hex.pm返回 403)——由 CI 证明它们。