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