Плейбук: CTR-тесты главной фотографии Ozon
Команды из чата: «сними данные до нового теста», «обнови тест», «статус CTR-тестов».
Источники истины и граница атомарности
- Рекламные факты и победители: серверная
/home/mi/Bot_TG/mp-system/mp.db. - Фото и Markdown: локальный
Product-Photo-Studio/Test CTR. - Веб читает только серверный mirror
/home/mi/Bot_TG/mp-system/data/ctr_tests, заданный черезCTR_TESTS_WEB_ROOT(допустим fallbackCTR_TESTS_MIRROR_ROOT). - DB capture не трогает файлы. Local materialize не вызывает API и не меняет серверную БД.
Между фазами нет ложной «распределённой транзакции»: каждая фаза идемпотентна и повторяется
с тем же
operation_id.
CLI никогда не запускает Alembic. Перед любой DB-командой он печатает resolved SQLite path и
сверяет его с обязательным абсолютным --expected-db. Роль также обязательна через
CTR_TESTS_DB_ROLE=server|local и совпадающий --db-role.
Рабочий цикл закрытия
1. Server DB capture
На Hetzner из корня проекта:
export CTR_TESTS_DB_ROLE=server
.venv/bin/python scripts/ctr_photo_tests.py close-db \
--database-url sqlite:////home/mi/Bot_TG/mp-system/mp.db \
--expected-db /home/mi/Bot_TG/mp-system/mp.db --db-role server \
--shop-id 4 --sku LT8307LS \
--operation-id close-lt8307ls-2026-08-21
.venv/bin/python scripts/ctr_photo_tests.py export \
--database-url sqlite:////home/mi/Bot_TG/mp-system/mp.db \
--expected-db /home/mi/Bot_TG/mp-system/mp.db --db-role server \
--file /tmp/ctr-tests-2026-08-21.json
close-db сначала обновляет campaigns, затем запрашивает только валидированные CPC РК товара.
Новая generic РК без артикула требует явный повторяемый --campaign-id. Текущий close принимает
только RUNNING/INACTIVE; исторический update-db диагностирует STOPPED/ARCHIVED отдельно.
Повтор команды с тем же operation_id возвращает существующую immutable revision и не создаёт дубль.
Окно не может быть будущим или длиннее 62 дней.
2. Передать JSON server → local
Передача выполняется отдельным scp/SFTP-шагом. ctr_photo_tests.py sync сети не использует.
Сверить SHA-256 перед импортом.
3. Local import + materialize
На Windows из корня Big-System-MP:
$env:CTR_TESTS_DB_ROLE = 'local'
.venv\Scripts\python.exe scripts\ctr_photo_tests.py import `
--database-url sqlite:///D:/YandexDisk/Bot_TG/Big-System-MP/mp.db `
--expected-db D:\YandexDisk\Bot_TG\Big-System-MP\mp.db --db-role local `
--file D:\Temp\ctr-tests-2026-08-21.json
.venv\Scripts\python.exe scripts\ctr_photo_tests.py materialize `
--database-url sqlite:///D:/YandexDisk/Bot_TG/Big-System-MP/mp.db `
--expected-db D:\YandexDisk\Bot_TG\Big-System-MP\mp.db --db-role local `
--shop-id 4 --sku LT8307LS `
--root "D:\YandexDisk\Bot_TG\Product-Photo-Studio\Test CTR"
Materialize безопасно и повторяемо:
- выводит имя папки только из
test_number, дат и state; имя из JSON не доверяется; - переименовывает
Last_DD-MMвTest_N_DD-MM_DD-MM, сохраняя фото; - атомарно заменяет
RESULT.md/README.mdчерез временный файл +os.replace; - создаёт следующий
Last_DD-MM; - на retry не удаляет существующие фото или RESULT;
- запрещает
.., вложенные SKU, symlink/junction/reparse points.
4. Local canonical → server web mirror
Сначала sync строит точную локальную staging-копию с manifest-prune. Корни не могут совпадать,
быть предками друг друга или содержать ссылки:
.venv\Scripts\python.exe scripts\ctr_photo_tests.py sync `
--root "D:\YandexDisk\Bot_TG\Product-Photo-Studio\Test CTR" `
--mirror-root "D:\Temp\ctr-tests-server-mirror"
Затем staging-копия переносится отдельным проверяемым deploy-шагом в
/home/mi/Bot_TG/mp-system/data/ctr_tests с точным mirror/delete semantics и SHA-256 сверкой.
После переноса перезапуск web не нужен, если CTR_TESTS_WEB_ROOT уже настроен.
Правила расчёта
- Размер снимается только как терминальный сегмент.
HZ20иHZ020— один alias-aware товар; identity case-insensitive. - При наличии
PLACEMENT_TOP_PROMOTION(«Поиск») берётся только он, иначеPLACEMENT_SEARCH_AND_CATEGORY(«Поиск и рекомендации»). Fallback при <100 кликов запрещён. - <100 кликов:
insufficient, папка всё равно архивируется. ≥100 и 0 корзин: валидный проигрыш, цена ∞. - Первый eligible период с корзинами — стартовый победитель. Новый побеждает только при том же плейсменте и campaign set, одновременно большем CTR и меньшей стоимости корзины.
- Победители отдельны по плейсментам. Сравнение: последние пять периодов плюс действующий победитель.
- День команды относится к старому фото; новый Last начинается D+1 по Москве. Текущий день может быть неполным. Отсутствие фото — предупреждение, отсутствие поддерживаемого плейсмента/API error — abort.
update-dbдобавляет immutable revision. Current winner детерминированно replay-ится; materialize пересобирает все MD товара и отдельно показывает исторический verdict и текущего победителя.
Migration release boundary
CTR migration c9a000a09 намеренно зависит от локальной warehouse c8a000a08. Развёрнутая серверная
ветка c7 → b7e2ad19c4f0 → c4e8f1a2d5b9 → d1f7a3c92e04 сохранена byte-for-byte. Обе ветки
сходятся только в merge e2a8c4f31b10. Деплой включает c8+c9, три серверные миграции и merge как
одну проверенную release boundary; app/models.py ради CTR не менять.
Финальная проверка
- DB path/role напечатаны и совпали с ожидаемыми;
- capture вернул operation ID + content digest;
- export/import digest совпал, open→closed envelope применён владельцу destination shop;
- фото сохранилось, RESULT/README и следующий Last существуют после повторного materialize;
- staging mirror не содержит удалённых локально managed-файлов;
- веб показывает только raster/MD, MD скачивается attachment, ответы имеют
nosniff; - server/local/staging SHA-256 сверены отдельным deploy-шагом.