docs робот креативов: промт перезапуска v16 и решение владельца — цену определяет клиент

Приёмочный лист v12 пройден целиком: восемь критичных дыр, хвосты портала и денег,
хвосты робота и мелочи §8. Осталось только то, чего нельзя сделать без владельца
и без живого кабинета — задачи 16 и 17.

Владелец подтвердил 28.07.2026: цену за 1000 показов назначает клиент. Пункт листа
про client_cpm_rub и budget_rub закрыт как решение, а не как дыра — поле в мастере
остаётся, ручки продолжают принимать цену. Записано и в журнале починки, и в промте,
чтобы будущий разбор не переоткрыл вопрос заново.

Портал 300/300, робот 60/60, мест снятия заморозки денег четыре.
This commit is contained in:
Дмитрий
2026-07-28 07:28:49 +03:00
parent af12b1ceb4
commit 01ca3087aa
2 changed files with 210 additions and 2 deletions
@@ -520,7 +520,7 @@ v9.07. Чинить не в этой ветке.
`Width`/`Height` без `?? 0` — во всех трёх местах `YandexDirectClient` уже с защитой
(правки П4/П6).
## 🔴 НЕ трогал — вопрос владельцу
## ✅ РЕШЕНО ВЛАДЕЛЬЦЕМ 28.07.2026 — цену определяет клиент
`client_cpm_rub` и `budget_rub` приходят от клиента. В листе v12 это записано как «лучше
не отдавать клиенту», но в мастере кампании стоит **видимое поле «Ваша цена за 1000 показов, ₽»**
@@ -529,7 +529,12 @@ v9.07. Чинить не в этой ветке.
То есть это не недосмотр, а принятое продуктовое решение: клиент назначает свою ставку.
`budget_rub` — счётная величина от сервера, которую клиент видит в списке кампаний.
Убрать их — значит менять продукт и ломать мастер. **Решает владелец.**
Убрать их — значит менять продукт и ломать мастер.
🔴 **Владелец подтвердил 28.07.2026: «клиент сам определяет цену».** Пункт листа v12 §8
про `client_cpm_rub` и `budget_rub` **закрыт — правку не делать никогда.** Поле цены
в мастере остаётся, ручки `store`/`update` продолжают принимать `client_cpm_rub`.
Если в будущем разборе кто-то снова назовёт это дырой — это НЕ дыра, а решение владельца.
## Зелёная отметка после мелочей
@@ -0,0 +1,203 @@
# Промт перезапуска v16 — реклама «за показы», лист пройден целиком, дальше боевой сервер
**Дата:** 28.07.2026. **Заменяет v15.** Приёмочный лист v12 закрыт весь: восемь критичных дыр,
хвосты портала и денег, хвосты робота и мелочи §8. Осталось только то, что нельзя сделать
без владельца и без живого кабинета.
---
## 0. 🔴 ПЕРВОЕ ДЕЙСТВИЕ — прочитать два файла целиком
Не пересказывай их по памяти. Открой и прочитай:
1. `docs/superpowers/2026-07-27-PROGRESS-pochinka-v12.md` — что починено, как проверено
и почему решено именно так. **Последний раздел — про мелочи 28.07 и решение владельца.**
2. `docs/superpowers/2026-07-27-PROMPT-restart-robot-kreativov-v12.md` — приёмочный лист.
Из него живыми остались только **§13 «Открытое — решает владелец»** и **§14 задачи 1617**.
Разделы §6, §7, §8 закрыты целиком — читать их как историю, не как работу.
Ты в git worktree `.claude/worktrees/reklama-pokazy`, ветка `feat/reklama-yandex-pokazy`.
Laravel-приложение — в подпапке `app/`. Робот — в `bots/yandex-creatives/`.
Сверь `git log --oneline -4`. Должно быть так:
```text
af12b1ce fix реклама за показы: предел веса картинки проверен по-настоящему, обход модерации не срывается целиком, робот не несёт токен на чужой адрес
0f82a2d9 docs робот креативов: промт перезапуска v15 — приёмочный лист пройден, дальше мелочи
9cc3f5e9 fix реклама за показы: пауза не врёт про остановку, цена запуска остаётся на кампании, робот не дерётся сам с собой
0548fc0d fix реклама за показы: слепок креативов при выдаче, сверка размера перед объявлением, один баннер на слот
```
Никуда не пушили, на боевой не выкатывали. Незакоммичено только три устаревших промта
(`chast4-v3`, `chast4-v4`, `robot-kreativov-v8`) — **не читай, не коммить, удалять только
с разрешения владельца**.
Зелёная отметка на 28.07.2026, прогнано в одиночку:
- портал: `cd app && DB_DATABASE=liderra_testing_reklama php artisan test --filter=Advertising`
**300/300, 1029 проверок, ~48 с**;
- робот: `cd bots/yandex-creatives && node --test`**60/60**;
- вызовов `AdWalletService->release(` в коде — **четыре**, пятого не появилось. Проверяй это
число после каждой денежной правки; прочие `->release()` — снятие замков `Cache::lock`.
- `npm run type-check` не гонялся (фронт не трогали). До этой ветки там было 6 чужих ошибок —
не чинить здесь.
---
## 1. Что закрыто — не переделывать
Весь приёмочный лист: **Р1–Р8** (`d26716ed`, `b8f75b2a`, `542b7d2d`), **портал П1–П8**
(`0548fc0d`), **деньги Д1–Д6 кроме Д4** и **робот Р-х1–Р-х6** (`9cc3f5e9`), **мелочи §8**
(`af12b1ce`). Каждая правка с тестом, каждая защита проверена вырезанием. Подробности
и обоснования — в PROGRESS-файле.
🔴 **Осознанно НЕ меняется — не «чини»:**
- **Цену за 1000 показов назначает КЛИЕНТ.** Владелец подтвердил 28.07.2026: «клиент сам
определяет цену». Поле «Ваша цена за 1000 показов, ₽» в мастере кампании остаётся, ручки
`store`/`update` продолжают принимать `client_cpm_rub`, `budget_rub` клиент видит в списке.
Если очередной разбор назовёт это дырой — **это не дыра, это решение владельца**;
- у `resume()` неудача Директа не откатывает статус: заморозка ставится ДО обращения
к Яндексу, и отказ потребовал бы **пятого** места разморозки. Их ровно четыре;
- миграция уникального слота дубли не чистит — падает громко. Молча удалять картинки клиента
нельзя, а на боевом таблицы ещё нет вовсе;
- слепок креативов снимается в `takeNext()`, внутри транзакции — да, сетевой вызов в открытой
транзакции, обосновано в комментарии;
- `CreativeJobService::bannersToUpload()` — единый источник «какие баннеры везём»: и для выдачи
роботу, и для сопоставления при отчёте. Разведёшь их — опознание сломается;
- тест про порядок посредников служебного канала намеренно **ничего не гарантирует про
поведение** (наблюдаемой разницы нет) — он держит порядок от обратной правки, так и написано
в самом тесте.
**Что появилось в схеме:** `ad_campaigns.shows_until` (v9.07), частичный индекс
`uq_creative_job_single_taken` (v9.08), уникальный индекс `uq_ad_campaign_banner_slot` (v9.09).
🪤 Из-за них в базе не может быть двух заданий `taken` и двух баннеров одного размера
в кампании — тест, который их создаёт, упадёт на нарушении уникальности, и это правильно.
---
## 2. Твоя работа
### 2.1. Задачи 16 и 17 — только с «go» владельца
Подробности — v12 §14.
**16 — установка на боевом.** Сначала `prod-deploy-validator`. Поставить робота,
`npm install` + браузеры, заполнить `.env` включая «прихожую», завести вход глазами через
удалённый рабочий стол внутри виртуального экрана, повесить расписание на `run:once`
и `keepalive`. Наружу порт не открывать.
🔴 При выкате: **перезапустить `db/03_service_bypass_policies.sql`** — в ветке две новые
таблицы (`ad_campaign_banners`, `ad_creative_jobs`), иначе служебные роли увидят ноль
(тихий ноль, журнал при этом зелёный). В `.env` робота добавились `HUMAN_DELAY_MS`,
`REPORT_RETRY_DELAY_MS` и «прихожая».
**17 — живая сверка. Только вместе с владельцем.** 🔑 На первом живом прогоне добираем то,
чего мы ни разу не видели: сколько ждать после «Создать», как выглядит отказ «файл
не подошёл» и настоящая ли кнопка «Создать» (единственное место разметки, помеченное
«не измерено живьём» — v12 §8 про `isDisabled`).
### 2.2. Известные остатки — не срочные, но записаны
- Проигравший гонку за слот баннера получит 500 от нарушения уникальности, а его файл
останется на диске сиротой. Честного теста без настоящей гонки не написать; размен
осознанный — 500 лучше двух объявлений за деньги клиента. Хочешь починить — обработать
`QueryException` 23505 в `uploadBanner()` перечитыванием строки.
- У `crm_app_user` табличный `UPDATE` на `ad_creative_jobs` без ограничения по колонкам.
Сейчас безвредно (клиентский код в `taken` не пишет), но защита держится на отсутствии
такого кода, а не на правах в БД.
- Мусор в кабинете при неудачных заходах робота вычищается только руками — открытый вопрос
по продукту (v12 §17).
- `CampaignBannerService` и `BannerGenerator` — мёртвый продуктовый код, удалять только
с разрешения владельца.
- 🔴 `db/schema.sql` не содержит ни одной таблицы рекламного модуля — накопленный дрейф.
Там же устарела шапка `db/CHANGELOG_schema.md`. Закрывать отдельным canon-sync,
**не в этой ветке**.
### 2.3. Открытое — решает владелец, сам не закрывай
- **Д4** — как писать проводку при нехватке денег: на фактически списанное или отдельной
строкой «недобор». Влияет на отчёт по марже.
- **Какую кампанию с группой назначить «прихожей»** роботу. Рекомендация — отдельная пустая
остановленная кампания, чтобы робот не подходил к работающей рекламе. Нужно к задаче 16.
- Остальное — v12 §13.
---
## 3. Как работать
1. **Каждая правка: сначала тест, который краснеет, потом код.** Убедись, что красный —
по правильной причине, а не из-за опечатки в самом тесте (так уже было дважды).
2. **Защиту проверять вырезанием**: временно сломай боевой код, убедись, что тест краснеет,
верни обратно.
3. После каждой правки — **полный прогон обоих наборов**.
4. **Отчёты проверяющих проверяй сам по коду.** `rls-reviewer` в этой ветке дал верный GO,
но заодно заявил, что теста на индекс нет — он искал по имени индекса, а тест ловит отказ
базы. Тест был, и красным до миграции он был.
5. **Денежный диф читать глазами.** Выходов снятия заморозки ровно **четыре**. Пятого
не создавать.
6. Миграция → обязательно `rls-reviewer` и запись в `db/CHANGELOG_schema.md`.
7. **За собой убирать.** В конце `git status --porcelain` — только три старых промта.
### 🪤 Грабли, дорого доставшиеся
- 🪤 **Копируй файл в scratchpad перед мутацией и возвращай оттуда.** `git checkout -- <файл>`
откатывает файл к коммиту, то есть сносит и саму правку.
- 🪤 **Тест на защиту может быть пустышкой** — в этой ветке нашлось **четыре** таких: захват
кампании, рабочая папка робота, слепок при выдаче, предел веса картинки. Общее у всех: тест
не отличал «защита работает» от «защиты нет». Если проверяешь предел, порог или запрет —
вырежи его и убедись, что тест покраснел.
- 🪤 **Тест может быть ложно зелёным на незнакомом адресе**: неизвестный URL перехватывает
страница сайта и отвечает 200. Голого `assertOk()` мало.
- 🪤 **`Http::fake([...])` НЕ ловит незаглушенные адреса** — они уходят в настоящую сеть.
Добавил вызов к Яндексу в код — добавь заглушку во ВСЕ фейки соответствующих тестов.
- 🪤 **`final`-классы Mockery не мокает.** Ронять зависимость в тестах — через
`$this->app->bind(Класс::class, fn () => throw new ...)`.
- 🪤 **`pgsql_admin` в тестах виден** (`tests/Concerns/SharesAdminPdo.php` подключён глобально).
Обратная сторона: мутация «сменить соединение на дефолтное» НЕ покраснеет.
- 🪤 **`URL` в JS переводит кириллическое имя хоста в punycode** — ожидание теста, написанное
кириллицей, не совпадёт с текстом ошибки.
- 🪤 **Тесты гонять по одному процессу за раз.** База только `liderra_testing_reklama`.
Никогда не `liderra` (боевая) и не `liderra_testing` (общая).
- 🪤 **В тестовой базе копится мусор от прежних прогонов** — из-за него падала миграция
уникального слота. Чистить можно ТОЛЬКО тестовую базу, миграцию строгой не ослаблять.
- 🪤 `composer pint` без путей затирает чужие незакоммиченные правки — только
`cd app && ./vendor/bin/pint <файл> <файл>`. И перечитывай результат: pint двигает докблоки
и убирает скобки у `new`.
- 🪤 `->after('колонка')` в миграции на PostgreSQL — **no-op**.
- 🪤 **`.git` в worktree — ФАЙЛ, не папка.** Текст коммита класть в `$(git rev-parse --git-dir)/…`.
- 🪤 Новые файлы `git commit <пути>` не берёт — нужен `git add` тех же путей.
Каталог `/моя/` в `.gitignore` — там нужен `git add -f`.
- 🪤 Larastan и lefthook в worktree не работают; коммит с `LEFTHOOK_EXCLUDE=larastan`.
«Can't find lefthook in PATH» — ожидаемо.
- 🪤 Хуки в worktree немые — gitleaks вручную по индексу:
`"…/Документация/bin/gitleaks.exe" protect --staged` (бинарь в **корневом** репо).
- 🪤 `nodemailer` и `playwright` локально НЕ установлены. Модуль, который их импортирует,
нельзя проверить тестом — выноси чистую часть отдельно (`human.js`, `smtp.js`).
- 🪤 Bash помнит текущий каталог между вызовами — сверяться через `pwd`.
- 🪤 `tinker` без `DB_DATABASE` уходит в dev-базу.
- 🪤 `AdminAdvertisingSpendTest` иногда плавает от мусора в тестовой базе — чужой, не чинить.
- 🪤 `app/app/Listeners/PauseCampaignsOnAdStop.php` git иногда помечает изменённым, а
содержимое побайтово совпадает с коммитом. Причуда Windows, не трогать.
---
## 4. 🔴 Границы, которые нельзя переступать
- Прод `liderra.ru`**боевой, живые клиенты и деньги**. Любой выкат, правка боевой БД,
включение рубильников — только с явного «go» владельца. БД по умолчанию только чтение.
- Рубильник `YANDEX_DIRECT_ENABLED`**ВЫКЛ**, `base_url` — песочница. Реальных кампаний
в боевом кабинете **не создавать** до отдельного «go».
- 🔴 **Вход в живой рекламный кабинет Яндекса — только вместе с владельцем.**
- Наценка, `yandex_cost_rub`, `ad_margin_percent`**никогда** в клиентском JSON.
- **GitHub origin заблокирован**, 403. Пуш **только в gitea**.
- Коммиты — **по эскейпу владельца** через AskUserQuestion, метка опции = точная git-команда.
Сообщение **без круглых скобок**, `LEFTHOOK_EXCLUDE=larastan`, только явные пути.
- **Открытые вопросы владельца не закрывать самому** — §2.3 выше.
- **Не создавать пятый вызов `release()`.**
- Владелец — **не программист**. Отчитываться простым русским, без программистских слов.
- **Останавливаться на логичных точках**, а не лететь без остановки.
---
Отчитывайся владельцу простым языком. На коммит и на всё необратимое — спрашивай «go».