Во втором коммите PR отдельный `git fetch origin main` был снят (из
job-контейнера git.gendsgn.ru недостижим, run 6977), эталон даёт сам checkout с
fetch-depth: 0. Комментарий про «тянут main отдельным шагом» остался и читался
бы дальше как факт.
Конфликт modify/delete по tradein-mvp/backend/data/sql/_manifest_applied.txt
разрешён удалением — удаление и есть предмет PR. За трое суток в main дописали
три имени (240/250/251); дописывать их некуда: файла больше нет, а гейт
tests/test_migration_numbering.py берёт эталон применённого из origin/main.
Проверено, что удаление ничего не оставляет без потребителя: манифест не *.sql,
цикл миграций в deploy-tradein.yml и bootstrap схемы в ci-tradein.yml берут
glob '*.sql', новый scripts/check-migration-lock-timeout.py — тоже.
Замер дрейфа на 27e199e3: 216 имён в манифесте против 232 файлов, отставание
16 (было 15 на 07.08). Старый гейт на этом дереве: 4 passed.
# Conflicts:
# tradein-mvp/backend/data/sql/_manifest_applied.txt
check-migration-lock-timeout.py требует lock_timeout только для NN >= 250 в
tradein — порог назначен по номеру аварийной миграции 250, которую затем
сняли с деплоя (#2792). Фактический максимум применённого на main — 239,
то есть весь диапазон 240-249 гейтом не проверяется вообще ("проверено
новых миграций: 0" = не проверено ни одного файла, не "все чисты"). Функция
scan() из самого гейта, прогнанная напрямую без порогового отсечения,
помечает ALTER TABLE в этом файле как блокирующий DDL без lock_timeout.
trade_in_estimates — самая горячая таблица стека (история, история
сотрудников, каждое чтение/PDF оценки); на этой БД уже наблюдались открытые
транзакции на 46 и 22 часа. Ждущая ACCESS EXCLUSIVE-блокировка встаёт в
очередь перед новыми запросами приложения. DDL на 1058 строках мгновенный —
риск не в исполнении, а в ожидании чужой блокировки.
Добавлено SET LOCAL lock_timeout = '5s' сразу после BEGIN + объяснение в
шапке файла, почему оно здесь при том что гейт формально не требует —
чтобы не убрали как "лишнее". Порог гейта не трогаю — отдельный issue.
main уехал вперёд за сутки: 234 занял 234_scrape_runs_ban_kind_unknown.sql
(0de22f4b), максимум на main сейчас 239 (235-237 — дыры). max+1=240 безопаснее
дыр; ни один открытый PR номер 235-240 не занимает (сверено по forgejo/main и
всем открытым веткам).
Переименован файл + обновлены все 7 упоминаний "migration 234"
(_manifest_applied.txt, config.py, schemas/trade_in.py,
purge_expired_trade_in_data.py, test_estimate_idor.py, content.ts,
types/trade-in.ts) — правки текстовые, ни один тест не читает миграцию по
имени файла.
Отдельный шаг `git fetch origin main` в backend-тестовых job'ах ронял прогон:
из job-контейнера git.gendsgn.ru:443 недостижим (run 6977, connection refused
за 5 мс), сеть есть только у самого checkout. Шаг и не был нужен — в логе того
же прогона видно, что при fetch-depth: 0 checkout идёт refspec'ом
`+refs/heads/*:refs/remotes/origin/*`, то есть origin/main появляется сам.
Refs #2683
_manifest_applied.txt по построению не мог покраснеть. Тест считал «новым»
любой файл, которого нет в списке, а новые файлы от списка освобождены
(докстринг test_manifest_covers_all_but_new_files: «НЕ требует, чтобы новый
файл уже был в manifest»). Забытое имя и новая миграция PR для гейта — одно и
то же, поэтому дрейф был не пропуском проверки, а её штатным исключением.
Замер на main 2026-08-07: 15 имён не дописано, все четыре теста зелёные —
через сутки после того, как #2692 догнал список руками.
Список при этом был лишь копией того, что git и так знает: deploy-tradein.yml
применяет КАЖДЫЙ data/sql/*.sql из main под ON_ERROR_STOP, то есть «файл
доехал до main» и есть «имя закреплено на проде». Ведём эталон в git — и
дрейфовать становится нечему.
Кросс-ветковая дыра закрыта тем же ходом: номер нового файла сверяется с
ПОЛНЫМ origin/main, а не с рабочим деревом, поэтому коллизия с миграцией,
смерженной после ветвления, находится. Проверено на живом PR #2754
(234_trade_in_estimates_retain_until против 234_scrape_runs_ban_kind_unknown
из main): старый гейт зелёный, новый красный.
Удаление/переименование применённой миграции сверяется с ТОЧКОЙ ВЕТВЛЕНИЯ, а
не с origin/main: иначе ветка недельной давности краснела бы за чужие
миграции. Проверено — ветка от 2026-07-30 при +43 миграциях в main зелёная.
CI: checkout переведён на fetch-depth 0 + отдельный fetch main. Этот Forgejo
не публикует refs/pull/N/merge (1620 */head, ноль */merge), а на depth=1 нет
ни origin/main, ни общего предка — без этого гейту не с чем сверять, и он
намеренно красный, а не тихо пропущенный.
Контракт сведён к одной формулировке — докстринг test_migration_numbering.py;
шапка манифеста, правило 3, хвост манифеста и рецепт из .claude/rules
удалены или заменены ссылкой. Заодно исправлен сам рецепт: `git ls-tree` без
`-r` печатает каталог, а не файлы.
Refs #2683
Deep-review MEDIUM: предполётная проверка purge_expired_trade_in_data считала
по базовому предикату без retain_until — здоровая оплаченная строка (retain_until
проставлен, платёж есть) через сутки после продажи тоже попадала под счётчик,
и джоба аварийно останавливалась на первой же честной продаже навсегда
(вместе с ней — и 180-дневное удаление лидов, вызываемое из той же функции
после этой проверки).
- _PREFLIGHT_PAID_CANDIDATES_SQL: добавлен терм `retain_until IS NULL` —
теперь считает только реальную аномалию (retain_until не проставлен, а
платёж есть), а не штатное состояние. Докстринги функции/модуля поправлены
под фактическое поведение.
- Тест на неверный инвариант (`"retain_until" not in sql`) заменён на
позитивный (`"retain_until IS NULL" in sql`) + добавлены live-DB тесты на
оба случая из ревью (здоровая оплаченная строка не поднимает тревогу,
джоба не блокируется).
- privacy/page.tsx: константа "12 месяцев" вынесена в content.ts
(PAID_REPORT_RETENTION_MONTHS) вместо литерала + расходящегося комментария;
добавлен сверяющий тест (test_paid_retention_text_consistency.py) по
образцу _CONSENT_TEXT_SNAPSHOT. Смягчена формулировка про автоматическое
удаление — задача на проде выключена и ни разу не запускалась, текст
теперь описывает установленный порядок, а не наблюдаемый факт.
- Все 10 висячих ссылок на untracked `mera-pr-d-spec.md` (7 файлов) заменены
на краткое изложение сути в комментарии + ссылку на PR #2754.
МЕРА: у 8 компонентов витрины v2 проп data больше не имеет дефолта из fixtures.ts — при сбое передачи данных компонент обязан упасть на TS-ошибке, а не отрисовать выдуманные числа на платном экране оценки. Цепная правка в SectionOverlay (4 поля стали обязательными в такт с детьми).
Птица: удалены 6 осиротевших компонентов (ноль импортов подтверждён репо-wide), подчищены 2 ссылающихся комментария.
Проверено ревьюером: tsc --noEmit и next lint реально отработали на 91b460b1 (лог задачи 18031), vitest 32/264 зелёные (лог 18033); storybook в репозитории отсутствует вовсе — «unwired/storybook usage» как обоснование дефолтов никогда не имело потребителя; ui-preview/estimate использует v1-компоненты со своей локальной фикстурой и не задет.
Мина: purge_expired_trade_in_data (сейчас enabled=false) удаляет строки
WHERE expires_at < NOW() AND created_by IS NULL — это ровно популяция
будущих платящих физлиц (владелец продаёт отчёт за 150 руб., отчёт должен
жить год на нашей стороне, а не 24ч). Первый прогон после запуска продаж
безвозвратно снёс бы оплаченное.
Делается ДО платёжного кода, которого в этом PR нет:
- migration 234: колонка trade_in_estimates.retain_until (NULL = неоплачено,
бэкенд-бита-в-бит не меняется) + частичный индекс под purge-предикат.
- config.py: trade_in_paid_retention_days=365 (ENV) — единственный источник
"12 месяцев" для будущей оферты/экрана/SQL продления.
- Единый гейт чтения ESTIMATE_READABLE_SQL + estimate_readable() — раньше
SQL-фильтр (404) и Python-проверка (410) в trade_in.py уже разошлись по
тексту ответа; текст "estimate expired (24h TTL)" убран (стал бы ложью при
годовом хранении).
- purge_expired_trade_in_data: retain_until IS NULL (не < NOW() — оплаченное
не удаляем в принципе) + NOT EXISTS(payments) как независимая страховка +
pre-flight, который считает оплаченных кандидатов и падает в mark_failed
ДО первого батча при ненулевом результате.
- PDF: "Ссылка доступна до …" только при retain_until IS NOT NULL;
"ДЕЙСТВИТЕЛЕН ДО" (expires_at, актуальность расчёта) не тронут.
- Фронт: retain_until прокинут в mapper (validUntil остаётся на expires_at).
- privacy-страница: убрано устаревшее "механизма удаления нет" (неправда
после #2547), добавлен срок 12 месяцев для оплаченных отчётов.
Ни строчки платёжного кода. expires_at, trade_in_estimate_retention_hours,
_DELETE_EXPIRED_LEADS_SQL не тронуты.
PII scrub wired to BOTH channels (before_send AND before_send_transaction) in app/main.py and app/workers/celery_app.py.
Before: Celery had no before_send at all, and before_send_transaction was URL-only while glitchtip_traces_sample_rate defaults to 0.05 - the Starlette integration puts request.data on transaction scope exactly as on error scope, so lead bodies leaked through the transaction channel.
Keys: full MERA set (client_name/client_phone/client_email/phone/email/name) plus company/message from PilotRequestInput.
VAT label: 'NDS (parking)' -> 'NDS (parking + commercial)' in DOCX/HTML exporters - financial.py computes VAT over parking AND non-residential.
Миграции 222 (DROP 2 tmp-таблиц + 5 строгих дублей индексов + v_data_quality с явным списком колонок) и 225 (CREATE INDEX CONCURRENTLY под FK listing_source_snapshots.run_id).
Проверено на прод-БД в BEGIN…ROLLBACK и на чистой схеме (полный bootstrap 225 миграций в одноразовом контейнере).
Deep review APPROVE (deep-code-reviewer, 2026-08-06).
HIGH закрыт: purge trade_in_estimates ограничен `created_by IS NULL` — 129 B2C-строк
под удаление, 911 пилотских защищены (сверено на проде: 1040 просрочено всего).
MEDIUM закрыт: телефон в erase_person_data сравнивается по каноническому РФ-виду
с обеих сторон (8→7 при 11 цифрах, без усечения до последних 10).
Проверено: миграции 229/231 прогнаны на прод-схеме в BEGIN…ROLLBACK, тело дважды —
идемпотентны; CHECK consent отбивает false; NN свободны на main и в открытых PR;
consent-гейт недостижим для B2B (session-cookie инжектит X-Authenticated-User);
адрес не попадает в БД раньше согласия ни одним путём.
Гейт: CI Trade-In / backend-tests success 3m9s на 4ee4d4b8.