ADR 0200 — The contract between modules becomes a versioned Arquiteto artifact, and the dev agent reads it instead of someone else's worktree
Status
Accepted. 2026-10-01 (AT-276, story HS-071, epic EP-030, round 36; owner
decision on 01/10: "the contract between modules becomes a versioned Arquiteto
artifact (in the mould of module_map/c4_diagram), read by the devs through
the read tool; changing a contract is a new Arquiteto version"). Extends the
table-less artifact design of
ADR 0065,
ADR 0068 and
ADR 0131; applies the rule "an agent
that writes must be able to read" (RN-164/165) to the dev agents, under the
contained reading of ADR 0060.
Context
The symptom
Real use of 2026-09-29 (analysis analise-uso-real.md, item A30, 6(e)):
dev-board-engine, seq 1157/1230/1268/1344/1486 — five turns reading interfaces
in other modules' worktrees. Against the AT-237 measurement
(medicao-do-jev.md), this agent had 24 steps
with a tool in the whole round: 5 of 24, ~21% of its steps, went into
discovering someone else's interface. The project had seven dev modules
(piece-catalog, retro-renderer, input-keyboard, game-session,
scoring, persistence, board-engine).
What could NOT be measured here: the token cost of those five steps. The
29/09 event log lives in the maintainer's local instance (compose brabo-dev)
and is not in this environment; the number above comes from the analysis and from
the agents table of the Jev measurement, not from a redone query. The cost gain is
measurable later, by the same rule as AT-239 (token_usage per actor).
What the product gave the dev (measured on dev, 2026-10-01)
- The
module_map(domain/architecture/module-graph.ts) is{name, stack, responsibility, dependsOn[]}. It says WHO depends on whom and nothing about WHAT a module offers. None of the Arquiteto's artifacts (module_map,project_image,module_routing,c4_diagram) carries an interface. - The dev's context (
Engine.Dev.ContextBuilder) brings story, task, business rules and ADRs filtered by module — no neighbour interface. - The dev's tools (
Engine.Dev.Tools) wereread_file,search_workspace,write_file,terminal,rag_search/rag_feedbackand the termination ones: no project artifact reading. The only way to see the neighbour's interface was theterminalreading the other dev's worktree — code IN PROGRESS on a branch that has not yet reacheddev, and that may change after the read.
Decision
- The contract is the
artifact.module_contractsartifact, the Arquiteto's. No table: the event is the record, versioned byversionin the payload, and the current one is the highestversionwith a tie broken byseq— the same reduction asGetC4DiagramUseCase/GetModuleRoutingUseCase. Each declaration carries the WHOLE list and REPLACES the previous one; changing a contract is a new version emitted by the Arquiteto, and the previous ones remain in the log. - The format is the smallest that solves it: per module, only what it
EXPOSES.
{modulo, expoe: [{tipo, assinatura, descricao}]}, withtipoinfuncao | rota | evento | dado, mandatoryassinatura(up to 300 characters), optionaldescricao, and from 1 to 40 items per module.- What it CONSUMES is not written in the contract. It is derived, at read
time, from the current
module_map'sdependsOn— the same argument as the C4 Container level (ADR 0068): the map already validated those edges as acyclic, and a second hand-written list would diverge from it at the first revised map. The granularity "which items of X module Y uses" was left out: the Arquiteto does not know it before implementation, and whoever consumes X reads the whole list of what X exposes. tipois an enum andassinaturais free text. The type is what changes how the item is USED; the signature does not fit a single schema because each module's stack is free (an Elixir function and a Go route).- The ceilings exist because the contract goes into every dev's context. A contract that becomes the whole API documentation would cost on every step what the worktree read cost on one.
- What it CONSUMES is not written in the contract. It is derived, at read
time, from the current
- An artifact SEPARATE from the
module_map, never one more field in it. The map is a table, is read by the whole execution (task claim, routing, C4) and revising it reopens cycle validation. The contract changes at another pace — the interface is discovered while the devs work —, and revising one cannot re-emit the other nor fight over its schema with another front (the round-36 resources lane adds a field to themodule_map). - The Arquiteto gains
declare_module_contracts(:direct, likeroute_modules_to_infra: an architecture declaration, with no external effect, never aproposed_action). The api validates and refuses with the whole reason, which goes back to the model (RN-061): a module outside the current map (listing the valid ones), nomodule_map, a repeated module, emptyexpoeor above the ceiling, unknowntipo, empty or too longassinatura. Its kickoff gains step 5; the tool entersEngine.Harness.IdiomaDaResposta.ferramentas_de_artefato/0(RN-623). - The dev agent gains
listar_contratos_de_modulos(:direct, in theEngine.Dev.Toolsregistry, outside the global one). CONTAINED reading: scope closed to the project by the route's path (GET internal/projects/:projectId/module-contracts), NO parameter, and the dev's module comes from the loop context (ctx.module, set byDevAgentServer), never from the model. With the module known, it shows in full only its own and those it consumes; those that consume it and the rest appear by name. A ceiling of 120 items in the text, with what was left out stated. A contract of a module that left the map is STATED, never attributed. - The dev's kickoff says the contract exists and where to read it, and that
another module's interface is not read in its worktree. Without a contract for
the module it needs, the tool says what to do: implement from what the story
and task say and, if the task depends on an interface that does not exist,
report_blockednaming the module and what is missing — for the Arquiteto to declare. It does not tell it to reread the worktree.
Consequences
- The step the dev spent discovering interfaces becomes a constant-cost read, and what it reads is the version the Arquiteto fixed, not another dev's in-progress code.
- The interface now has an owner and a version. Whoever changes it is the Arquiteto, in a new version; the dev that needs a change does not make it in the neighbour's code, it blocks naming what is missing.
- It is not a lock. Nothing prevents the
terminalfrom reading another worktree: the tool and the kickoff change the path the dev knows, they do not forbid the other. Closing the reading of someone else's worktree would be path containment (ADR 0055), another front. - The Arquiteto gains a step in the kickoff, under the SAME ceiling of 14
iterations. Not measured whether it fits together with the other seven in a
large architecture; exhausting the ceiling is still narrated
(
toolloop.limit_reached, RN-166). - Projects without a contract keep working: the read returns the map with
expoe: nullon each module and the notice. No migration, no table. - The artifacts'
docs/folder (ADR 0148) gains the type as versioned: the current one overwrites the same file, like the other four. - Out of here, declared: its own screen (the contract appears on the
timeline and in the
docs/folder, not on the Architecture tab); the effect on Jev's gain (AT-239 measures tool per step; this one changes the dev's menu, andscripts/jev/catalogo.jsonis the 29/09 snapshot, not regenerated here); the ExUnit tests ran outsidemix(repo.hex.pmgives 403 in this environment) — CI proves them.