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

ADR 0133 — Infra выбирает среди кандидатов маршрутизации, и выбор становится новой версией artifact.project_image

Контекст​

ADR 0131 открыл недостающую половину маршрутизации модулей: Arquiteto ВЫДВИГАЕТ образ на каждый модуль (route_modules_to_infra, записанный в artifact.module_routing) и прямо заявил, что «половина, которая ВЫБИРАЕТ (Infra Lead читает этот список и решает, с собственным proposed_action), — это отдельный PR». Это тот самый PR.

ADR 0130 (broker контейнеров) тоже оставил висящую фразу, в самом комментарии ContainerBrokerPort: «Поднять, остановить и удалить — это ВНЕШНИЙ ЭФФЕКТ, и они не происходят без proposed_action; предлагать будет Infra Lead, с окончательными полномочиями пользователя, и это другой PR». Тоже этот.

Вопрос, который оба ADR оставили открытым и который появляется только при попытке СОЕДИНИТЬ две нити, таков: broker не принимает спецификацию — он читает GET /internal/projects/:projectId/container-spec (ObterSpecDeContainerUseCase), который, в свою очередь, читает действующее решение Arquiteto в artifact.project_image (RN-105, DecidirImagemDoProjetoUseCase, ADR 0065). Он НЕ читает artifact.module_routing. Итак: если Infra выбирает кандидата маршрутизации, и это не становится новой версией artifact.project_image, выбор проверяем в event log и инертен — поднимающийся контейнер остаётся тем, что решил choose_project_image, которого может ещё не быть или который может быть совсем другим образом, нежели выбранный.

Решение​

Выбор Infra испускает НОВУЮ ВЕРСИЮ artifact.project_image, целиком повторно используя DecidirImagemDoProjetoUseCase, — а не параллельный путь.

Это значит: artifact.project_image перестаёт испускаться только Arquiteto (choose_project_image). У него появляется ДВА возможных источника — Arquiteto, решающий с нуля, и Infra, ВЫБИРАЮЩАЯ среди кандидатов маршрутизации, которые произвёл сам Arquiteto, — и decidedBy каждого события говорит, кто именно ('arquiteto' vs. 'infra-lead', поскольку DecidirImagemDoProjetoUseCase.execute уже принимал этот параметр, просто никогда не использовал его с другим значением). Комментарий в коде project-container.ts, называющий образ «АРТЕФАКТОМ ARQUITETO», остаётся верным в важном смысле — это архитектурное решение, версионируемое, проверяемое, никогда не скрытая конфигурация, — но перестаёт быть верным в смысле «пишет в него только Arquiteto». CLAUDE.md (раздел Соглашения) обновляется, чтобы объявить это.

Почему повторно использовать DecidirImagemDoProjetoUseCase, а не параллельный путь, записывающий artifact.project_image напрямую:

  1. Та же проверка, один раз. validarDecisaoDeImagem (явный tag/digest, отказ от latest, минимальный rationale, потолок RECURSOS_MAXIMOS) уже является единственным источником этого правила — повторная реализация на пути Infra создала бы ТО ЖЕ расхождение, которое комментарий в module-routing.ts уже зафиксировал, решив повторно использовать эту функцию, а не переписывать её для каждого модуля. То же рассуждение, на уровень выше.
  2. Один действующий, а не два. ObterContainerDoProjetoUseCase читает «версию с наибольшим номером» artifact.project_image — если бы выбор Infra записывался в другой тип события или в собственную таблицу, у продукта появилось бы ДВА возможных ответа на вопрос «какой образ запускает этот проект», и каждому существующему потребителю (RegistrarTransicaoDeContainerUseCase, шлюз RN-105 вкладки Code, broker через ObterSpecDeContainerUseCase) пришлось бы учиться выбирать между ними. Повторное использование того же артефакта — то, что сохраняет «один действующий», не требуя ни от кого изменений.
  3. След уже существует. rationale — свободное поле: выбор записывает в него, что образ был ВЫБРАН среди кандидатов маршрутизации (со ссылкой на porque, которое дала Infra), поэтому тот, кто читает историю artifact.project_image месяцы спустя, не видит образ, появившийся из ниоткуда: он видит цепочку — Arquiteto выдвинул, Infra выбрала, с мотивом каждой стороны.

Выбранный образ ДОЛЖЕН быть одним из imagemCandidata действующего artifact.module_routing — никогда выдуманным. ExecuteContainerStartUseCase проверяет это ДО вызова DecidirImagemDoProjetoUseCase: если Infra предлагает образ вне списка, действие проваливается с сообщением, называющим отклонённый образ и допустимых кандидатов, — тот же механизм объясняющего отказа, который уже используют validarRoteamento/validarDecisaoDeImagem (RN-061: вся причина возвращается тому, кто решает исправить). Без действующей маршрутизации (status: 'sem_roteamento') нет ни одного кандидата, и любой выбор отклоняется — Arquiteto должен сначала выполнить route_modules_to_infra.

Явное, не скрытое напряжение: Arquiteto остаётся тем, кто ПРЕДЛАГАЕТ (через choose_project_image ИЛИ через route_modules_to_infra); Infra получает возможность испускать новую версию ТОГО ЖЕ артефакта, когда ВЫБИРАЕТ. Это не «Infra решает образ с нуля», а «Infra выбирает, какое из предложений Arquiteto становится реальностью», и запись отражает это: decidedBy: 'infra-lead' и rationale, ссылающийся на исходную кандидатуру, никогда не rationale, выдуманный с нуля, как если бы это было новое решение.

container_start — новый proposed_action, с окончательными полномочиями пользователя — никогда не автоодобряемый по умолчанию. В отличие от open_infra_pr (который Infra Lead может автоодобрить сам себе, потому что он лишь открывает PR, который человек ещё смержит), подъём контейнера — настоящий внешний эффект: он вызывает broker, который говорит с демоном Docker сервера (ADR 0130). Он остаётся require_approval по умолчанию (MIN_ROLE_FOR_ACTION_TYPE: maintainer, тот же калибр, что у open_infra_pr/parallelize), и НЕ входит в список автономии, засеиваемый accept-handoff Infra Lead, — намеренное решение, а не забывчивость.

container_start остаётся ВНЕ блока абсолютных потолков decide.ts (merge в защищённую ветку, instruction_patch, parallelize/ raise_max_parallel) — то же рассуждение, уже зафиксированное там для propose_execution_plan/assess_implementability: это ПЕРВЫЙ раз, когда контейнер этого проекта действительно поднимается для этого выбора, а не превышение уже разрешённого потолка. maintainer МОЖЕТ настроить permissions.json на автоодобрение container_start, если решит, что хочет такого уровня автоматизации, — продукт не закрывает эту дверь силой, так же как не закрывает её для open_infra_pr.

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

Шлюз RN-105 не меняется. Вкладка Code остаётся закрытой, пока у artifact.project_image нет решения, — только теперь это решение может прийти из двух мест. Ни одному существующему потребителю ObterContainerDoProjetoUseCase не пришлось меняться.

container_start — ПЕРВЫЙ реальный вызывающий ContainerBrokerPort.start. Из пяти операций ADR 0128/0130 у inspect уже был вызывающий (чтение, маршрут жизненного цикла); start получает своего здесь. stop, remove и exec остаются без какого-либо proposed_action — это не пробел, закрытый целиком, а закрывающийся кусок отсечения ФАЗЫ 25b.

Dev agents ВСЁ ЕЩЁ не работают внутри поднимаемого контейнера. Поднять контейнер и перенести worktree dev agent внутрь него — разные вещи; второе — отдельный PR (1.6). Этот PR доказывает, что путь proposed_action → одобрение → broker → project_containers работает от начала до конца; внутри контейнера пока выполняется только то, что поднимает broker (выбранный образ, монтирующий папку проекта), — пока без потребителя.

RegistrarTransicaoDeContainerUseCase получает первого реального вызывающего вне тестов. Машина состояний уже существовала (ADR 0081); новое в том, что теперь что-то помимо теста по-настоящему запускает provisioning/running, после подтверждения от broker. Переход в provisioning возможен только когда строки ещё нет или строка в failed/removed — из stopped правильный переход — сразу в running (машина состояний ADR 0081 не допускает stopped -> provisioning, а пересоздание строки с нуля для контейнера, который был просто остановлен, без нужды уничтожило бы историю imageVersion).

Рассмотренная и отвергнутая альтернатива: новое событие (artifact.container_election или подобное), отдельное от artifact.project_image. Оно сохранило бы более чистое разделение между «Arquiteto предлагает» и «Infra решает», но создало бы ровно ту проблему, которую избегает Решение: два артефакта-кандидата на «действующий образ этого проекта», и каждому потребителю (broker через ObterSpecDeContainerUseCase, RegistrarTransicaoDeContainerUseCase, шлюз RN-105) пришлось бы уметь выбирать между ними. Отвергнуто, потому что цена поддержания двух действующих в синхронизации превышает ценность более «чистого» разделения двух ролей — ОБЕ роли продолжают существовать (Arquiteto предлагает, Infra выбирает), лишь выходной артефакт один.