---
verdict: APPROVED
work_item: ORCH-103
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-11
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-103
version: 1
---

# Review ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт

> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter.

## Summary

PR закрывает Type B эпика ORCH-10 строго по ТЗ и ADR-001 (D1–D11): новый каталог
`deploy/bundled/` (самодостаточный compose 16 сервисов, project name `orchestrator-bundle`,
пиннинг неподвижными тегами, одна bridge-сеть, только человеческие порты, мина
`GITEA__webhook__ALLOWED_HOST_LIST` закрыта), `scripts/bootstrap_bundle.py` (stdlib-only,
`plan`-дефолт/`apply`/`verify`, step-движок check→ensure, exit 0/2/1, ноль delete-операций),
конфиг-канон `deploy/bundled/.env.example` (ни одного дефолтного пароля),
`docs/deployment/BUNDLED_SETUP.md` (14 разделов канона D10) и три содержательных
анти-дрейф тест-модуля. Рантайм байт-в-байт: `git diff main` не содержит `src/**`,
корневого `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/**` (AC-6 подтверждён
diff-stat'ом). Полный регресс: **`pytest tests/ -q` → 1844 passed, 0 failed** (AC-5);
существующие анти-дрейф тесты (`test_lite_setup_doc.py`, `test_no_host_hardcodes.py`,
канон ORCH-009) не правились. Документация обновлена в том же PR по всем точкам §8 ТЗ.
Findings — только P2/P3, блокеров нет → **APPROVED**.

## Оси проверки

### 1. Соответствие ТЗ (02-trz.md, 03-acceptance-criteria.md)

- **FR-1** ✅ — отдельный bundle-compose; состав = ADR D1/D3 (тест
  `test_bundle_has_exactly_the_adr_service_set` фиксирует множество); пиннинг всех сторонних
  образов литералом (TC-03); тома — именованные с префиксом проекта + bind строго внутри
  project dir (TC-04); достижимость в обе стороны — сервис-DNS (D4) + `ALLOWED_HOST_LIST`;
  карта портов в доке §2, конфликт порта → отказ preflight; staging-контура нет вовсе.
- **FR-2** ✅ — последовательность шагов 1–9 ТЗ воспроизведена 1:1 в `APPLY_STEPS`
  (тест `test_apply_steps_match_normative_plan` держит соответствие нормативному плану);
  идемпотентность — check→ensure/skip (AC-8 покрыт unit'ами resume/«противоречивое
  состояние»); exit-контракт 0/2/1 (`test_exit_code_contract`); манифест manual-step честный:
  инструкция → подтверждение → API-верификация, без TTY — немедленный exit 2.
- **FR-3** ✅ — webhook-секреты строго субпроцессом `gen_secrets.py` (структурный тест);
  bundle-креды — stdlib `secrets` (token_hex ≥16 байт, unit проверяет длину); в репо только
  пустые плейсхолдеры (`test_bundle_secrets_in_example_are_empty_placeholders`); права 600;
  без перетирания без `--force-secrets` (`test_merge_missing_secrets_never_overwrites_without_force`).
- **FR-4** ✅ — BUNDLED_SETUP.md: все 14 разделов в порядке маршрута, fenced-команды +
  «Проверка:»/PASS/FAIL в каждом исполняемом разделе, общие шаги ссылками на
  LITE_SETUP §5–§8 / ONBOARDING / REPLICATION §4 (форк канона отсутствует), fail-closed имена
  `Confirm Deploy`/`STOP` и «22 статуса» — сверкой импорта `plane_sync._PLANE_NAME_TO_KEY`.
- **FR-5** ✅ — три модуля без docker/сети/LLM; FORBIDDEN — импорт из
  `test_no_host_hardcodes.py` (один источник истины); секрет-эвристика hex≥32/alnum≥40 с
  негативным самочеком (не-evergreen); key-set-sync `${VAR}` ⊆ bundle-канона; заморозка
  корневого compose зеркалом ассерта ORCH-102.
- **FR-6** ✅ — smoke = REPLICATION §4 поверх bundle (§11 дока, без форка), минимальный
  сигнал «артефакты 01–04 в ветке» зафиксирован.
- **AC-матрица:** AC-4…AC-9 в файловой/структурной части — PASS (TC-01…TC-12 зелёные).
  AC-1/AC-2/AC-3/AC-8(повторный прогон) в e2e-части — **ручная приёмка** на чистом хосте/VM
  по рамке самих AC (в CI docker/LLM не гоняются) — остаётся за стадией приёмки, см. P2-2.

### 2. Соответствие ADR (06-adr/ADR-001, adr-0038)

- D1–D11 реализованы без отклонений; сквозной `adr-0038-bundled-replication-canon.md` заведён.
- **Прогрессивная автоматизация webhook (D7)** — единственное место, где реализация глубже
  буквы ADR: `step_plane_webhook` регистрирует workspace-webhook прямой записью в Postgres
  инсталляции. Сверено: это **не новый канон, а переиспользование** документированного
  «пути Б» LITE_SETUP §5.4 (тот же INSERT-контракт, колонка-в-колонку), контракт чекпоинта
  сохранён (верификация SELECT'ом; при отказе — fallback на честный manual-step с той же
  проверкой), схема стабильна благодаря пину `v0.23.1`. D7 явно разрешает такую замену
  manual-step → ensure «без правки ADR». Нарушения нет; в доке §7 чекпоинт 4 описан честно.
- **Трассировка (TRACEABILITY):** правка чужого маркированного блока одна — строка Type B в
  `REPLICATION.md` §1 (артефакт ORCH-101); это запланированная точка расширения, прямо
  предписанная ТЗ FR-6/§8 — инвариант ORCH-101 не сломан. Остальные изменения аддитивны
  (новые файлы / новые секции CLAUDE.md, README архитектуры, CHANGELOG).
- Нормативы предшественников соблюдены: branch protection НЕ настраивается (D10 ORCH-009 /
  INV-4 — явно в D6 и §14.6 дока), compose не форкается (adr-0037), «дефолт = боевое» не
  нарушен (корневой `.env.example` не тронут).

### 3. Качество кода

- `pytest tests/ -q`: **1844 passed, 0 failed** (66s); новые тесты содержательные — unit'ы
  чистых функций покрывают позитив/негатив/resume/противоречивое состояние, эвристики имеют
  негативный самочек, ast-скан stdlib-allowlist реально закрыт.
- Бизнес-логика скрипта аккуратная: never-print секретов в stdout (только имена ключей),
  маскированный лог при падении clone (токен в URL не утекает в вывод), `_psql` через stdin
  (секрет не в argv), fail-fast preflight до любых мутаций, диагностика «кто не дождался +
  хвост логов». Не багфикс-трек (feature) — требование регресс-теста-фиксатора BR-4/ORCH-019
  неприменимо.
- Замечания P2/P3 — ниже; ни одно не ломает инварианты конвейера и не относится к рантайму
  платформы (скрипт исполняется только оператором на целевом хосте).

### 4. Документация — обязательная проверка

Выполнена явно, см. раздел «Документация» ниже. Обновлено всё требуемое в том же PR.

## Findings

### P0 — Blocker
- (нет)

### P1 — Must fix
- (нет)

### P2 — Should fix
- [ ] **P2-1. Секреты в argv субпроцессов** (`scripts/bootstrap_bundle.py`):
  `step_init_gitea` передаёт `GITEA_ADMIN_PASSWORD` аргументом `--password` в
  `docker compose exec gitea gitea admin user create …`, а `step_agent_git` — токен в
  clone-URL аргументом `git clone http://oauth2:<token>@…`. Значения видимы в `ps` хоста на
  время исполнения. Формально AC-8 («секрет виден в stdout/логе») не нарушен, но это против
  духа NFR-3 (ТЗ FR-2) и непоследовательно с собственной argv-гигиеной скрипта (`_psql`
  прогоняет секреты через stdin с явным комментарием «секреты не попадают в argv, NFR-3»).
  Рекомендация: для clone — использовать `git -c credential.helper`/`GIT_ASKPASS` либо
  дописать компромисс в TR-7 (10-tech-risks) и шапку скрипта; для `gitea admin user create`
  альтернатив CLI мало — минимум зафиксировать окно экспозиции в TR-7. Не блокер: разовая
  операция оператора на одноарендном целевом хосте, угроза-модель совпадает с уже
  зафиксированным компромиссом TR-7.
- [ ] **P2-2. Замер цифр «Требований к хосту» отложен на приёмку** (AC-4): BUNDLED_SETUP §2
  декларирует «подтверждаются замером приёмочного развёртывания» — на момент ревью цифры
  (8 GB / 40 GB / 4 vCPU) синхронизированы с константами preflight структурным тестом, но
  фактический замер ещё не зафиксирован. По рамке 03-acceptance-criteria (e2e — ручная
  приёмка вне CI) это допустимо, однако при ручной приёмке AC-1/AC-2 результат замера нужно
  зафиксировать (13-test-report / 15-staging-log или правка §2), иначе FAIL-условие AC-4
  «цифры с потолка» останется формально незакрытым.

### P3 — Nice to have
- [ ] **P3-1.** `step_plane_webhook`: `slug`/`secret` интерполируются в SQL-строку без
  экранирования одинарных кавычек. Секрет — hex от `gen_secrets` (безопасен), slug —
  операторский ввод; кавычка в slug уронит INSERT. Риск минимален (ON_ERROR_STOP +
  fail-safe fallback на manual-step), но дешёвое `value.replace("'", "''")` сняло бы класс
  целиком.
- [ ] **P3-2.** `run_verify`: при одновременном health-FAIL и onboard `exit 2` функция
  возвращает `EXIT_MANUAL` (2), маскируя ошибку (ожидался бы приоритет `1`). Поведенческая
  мелочь read-only режима.

## Документация

| Артефакт | Статус |
|----------|--------|
| `CLAUDE.md` | ✅ новый раздел «Bundled-тираж (ORCH-103)» (паттерн ORCH-101/102) |
| `docs/architecture/README.md` | ✅ блок Type B — Bundled рядом с 10-common/Lite |
| `CHANGELOG.md` | ✅ запись `feat: ORCH-103` (детальная, D1–D11) |
| `docs/operations/REPLICATION.md` §1 | ✅ Type B → ✅ ORCH-103 + ссылка на BUNDLED_SETUP.md |
| `docs/deployment/BUNDLED_SETUP.md` | ✅ создан, 14 разделов канона D10, держится тестом |
| ADR | ✅ work-item `06-adr/ADR-001-…` + сквозной `adr-0038-bundled-replication-canon.md` |
| `07-infra-requirements.md`, `10-tech-risks.md` (TR-1…TR-9) | ✅ заведены архитектором, кросс-рефы из кода сходятся |
| `.gitignore` | ✅ `deploy/bundled/repos/` (NFR-3) |
| `README.md` «Известные ограничения» (ORCH-079) | ✅ проверено явно: PR не закрывает ни один из 3 открытых пунктов (Telegram 48h / intra-repo deps / пакетный автоном) — обновление витрины не требуется |

`src/**` не изменён (PR — deploy/scripts/docs/tests), поэтому P0-правило «`src/` изменён без
документации» неприменимо; документация при этом обновлена полностью.

## Итог

`verdict: APPROVED` — P0/P1 отсутствуют; P2-1/P2-2 и P3 рекомендуется снять follow-up'ом или
при ручной приёмке Bundled-развёртывания (AC-1/AC-2/AC-3/AC-8 e2e-часть).
