Перейти к основному содержимому

ADR 0200 — Контракт между модулями становится версионируемым артефактом Arquiteto, и dev agent читает его вместо чужого worktree

Статус​

Accepted. 2026-10-01 (AT-276, история HS-071, эпик EP-030, раунд 36; решение владельца от 01.10: «контракт между модулями становится версионируемым артефактом Arquiteto (по образцу module_map/c4_diagram), читаемым devs через инструмент чтения; изменение контракта — новая версия от Arquiteto»). Расширяет дизайн артефакта без таблицы из ADR 0065, ADR 0068 и ADR 0131; применяет правило «агент, который пишет, должен иметь возможность читать» (RN-164/165) к dev agents, под сдержанным чтением из ADR 0060.

Контекст​

Симптом​

Реальное использование 2026-09-29 (анализ analise-uso-real.md, пункт A30, 6(e)): dev-board-engine, seq 1157/1230/1268/1344/1486 — пять ходов, читающих интерфейсы в worktrees других модулей. По замеру AT-237 (medicao-do-jev.md) у этого агента было 24 шага с инструментом за весь раунд: 5 из 24, ~21% его шагов, ушли на выяснение чужого интерфейса. В проекте было семь dev-модулей (piece-catalog, retro-renderer, input-keyboard, game-session, scoring, persistence, board-engine).

Что здесь НЕ удалось измерить: стоимость этих пяти шагов в токенах. Event log от 29.09 живёт в локальном экземпляре сопровождающего (compose brabo-dev) и в этом окружении отсутствует; число выше взято из анализа и таблицы агентов замера Jev, а не из повторённого запроса. Выигрыш в стоимости измерим позже, той же линейкой, что у 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, читающий worktree другого dev, — код В РАБОТЕ в ветке, которая ещё не дошла до dev и может измениться после чтения.

Решение​

  1. Контракт — это артефакт artifact.module_contracts от Arquiteto. Без таблицы: событие является записью, версионируемой по version в payload, а действующий — тот, у которого наибольший version с разрешением ничьей по seq, — то же сведение, что у GetC4DiagramUseCase/GetModuleRoutingUseCase. Каждое объявление несёт ВЕСЬ список и ЗАМЕНЯЕТ предыдущий; изменение контракта — новая версия, испущенная Arquiteto, а предыдущие остаются в log.
  2. Формат — минимальный, который решает задачу: по модулю, только то, что он ПРЕДОСТАВЛЯЕТ. {modulo, expoe: [{tipo, assinatura, descricao}]}, с tipo из funcao | rota | evento | dado, обязательной assinatura (до 300 символов), опциональным descricao и от 1 до 40 элементов на модуль.
    • То, что он ПОТРЕБЛЯЕТ, в контракт не записывается. Это выводится при чтении из dependsOn действующего module_map — тот же аргумент, что для уровня Container C4 (ADR 0068): карта уже проверила эти рёбра на отсутствие циклов, и второй список, написанный вручную, разошёлся бы с ней при первой пересмотренной карте. Детализация «какие элементы X использует модуль Y» осталась вне: Arquiteto не знает её до реализации, а тот, кто потребляет X, читает весь список того, что X предоставляет.
    • tipo — enum, а assinatura — свободный текст. Тип — то, что меняет способ ИСПОЛЬЗОВАНИЯ элемента; сигнатура не умещается в единую схему, потому что стек каждого модуля свободен (функция Elixir и маршрут Go).
    • Потолки существуют потому, что контракт идёт в контекст каждого dev. Контракт, ставший всей документацией API, стоил бы на каждом шаге того, чего стоило чтение worktree за один.
  3. Артефакт ОТДЕЛЬНЫЙ от module_map, никогда не лишнее поле в нём. Карта — это таблица, её читает всё выполнение (claim задачи, маршрутизация, C4), и её пересмотр заново открывает проверку циклов. Контракт меняется в другом ритме — интерфейс обнаруживается, пока devs работают, — и пересмотр одного не может переиспускать другое или оспаривать его схему с другим направлением (lane ресурсов раунда 36 добавляет поле в module_map).
  4. 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).
  5. Dev agent получает listar_contratos_de_modulos (:direct, в реестре Engine.Dev.Tools, вне глобального). СДЕРЖАННОЕ чтение: область, замкнутая на проекте через путь маршрута (GET internal/projects/:projectId/module-contracts), НИ ОДНОГО параметра, а модуль dev берётся из контекста цикла (ctx.module, устанавливается DevAgentServer), никогда от модели. При известном модуле он показывает целиком только свой и те, которые он потребляет; те, кто его потребляет, и остальные появляются по имени. Потолок 120 элементов в тексте, с указанием того, что осталось вне. Контракт модуля, вышедшего из карты, ОБЪЯВЛЯЕТСЯ, никогда не приписывается.
  6. Kickoff dev говорит, что контракт существует и где его читать, и что интерфейс другого модуля не читается в его worktree. Без контракта для нужного модуля инструмент говорит, что делать: реализовывать по тому, что говорят story и task, и, если task зависит от несуществующего интерфейса, report_blocked, называя модуль и то, чего не хватает, — чтобы Arquiteto объявил. Не отправляет перечитывать worktree.

Последствия​

  • Шаг, который 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/, а не на вкладке Архитектура); эффект на выигрыш Jev (AT-239 измеряет инструмент на шаг; этот меняет меню dev, а scripts/jev/catalogo.json — снимок от 29.09, здесь не перегенерированный); тесты ExUnit запускались вне mix (repo.hex.pm даёт 403 в этом окружении) — их доказывает CI.