4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 721ceb9876 |
fix(tradein): выборка миграции 286 повторяет гейт 1:1, правило первой точки (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m56s
Ревью нашло, что миграция и код ловили РАЗНОЕ. Миграция брала базой предыдущую СЫРУЮ строку (lag), гейт — предыдущую ОСТАВЛЕННУЮ. На 1M→10M→1M→10M (цена 1M) lag-версия удаляла честную точку, на 1M→10M→1.05M→9.9M — не была идемпотентной (второй прогон доедал 9.9M). Теперь кандидаты выбирает PL/pgSQL-цикл, пошагово повторяющий drop_decimal_slips, а правило первой точки — отдельным INSERT..SELECT уже по ОСТАВШИМСЯ строкам. Правило первой точки — из прод-разбора: 12 из 20 остатков domklik это серии вида 330 000 → 3 300 000 (текущая цена 3 300 000) и 420 000 → 4 200 000 → 4 500 000, где дефектная точка ПЕРВАЯ и базы слева у неё нет. Свидетелей по-прежнему два: ×10 ко второй точке И подтверждение второй третьей-или-текущей-ценой. Решение по первой точке принимается по kept-серии, а не по сырой, — иначе гейт теряет идемпотентность (перебор ловит 1122 таких прогона). Идемпотентность доказана НА ГЕЙТЕ: property-тест gate(gate(s)) == gate(s) по всем сериям длины 2-6 (19 525 серий × 3 текущие цены). Фальсифицирован обеими поломками — сырая база даёт 136 красных прогонов, сырые соседи первой точки 1122. Раз SQL зеркалит гейт, свойство переносится на миграцию. Ещё в 286: третий свидетель ПРОТИВ удаления (цена подтверждена триггерной строкой того же объявления — значит она реально наблюдалась в listings.price_rub) и финальный шаг |diff_percent| > 100 → NULL по всем источникам, то же правило, что validate_diff_percent на записи. Ожидаемое число удалений в шапке — 35 + ~12 из двухсвидетельского предзамера, а не 76 (то была односвидетельская цифра). Прогон обеих фаз дважды с ROLLBACK — tradein-mvp/scripts/sql/286_dryrun.sql. yandex: проводка гейта снята как мёртвая. На том пути серия из двух точек, а свидетель последней — текущая цена лота, то есть она же сама: ветка по построению не могла выбросить ничего. Оставлен честный комментарий-потолок и ссылка на follow-up (отлов требует DELETE на следующем наблюдении). cian: после выброса точки соседу пересчитывается diff_percent (было только у domclick). domclick: цена листинга берётся RETURNING'ом у UPDATE вместо отдельного SELECT по PK, пересчёт вынесен в общий recompute_diff_percent с гейтом на пустую цену (ручной ingest кладёт price_changes из JSONL без валидации). |
|||
| 142967d064 |
fix(tradein): гейт против сдвига разряда в offer_price_history (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
В priceHistory источников встречаются точки ровно ×10/÷10 к соседям с возвратом к базе следующей же точкой — это потерянный разряд у источника, а не рынок. Прод-замер 06.09.2026: 76 таких строк у 55 объявлений (domklik 48, cian 21, yandex 6; единственная avito-строка — триггерная). Читатели колонки — медианный торг лендинга (#3223), админка, /scrapers. drop_decimal_slips живёт рядом с validate_diff_percent — на той же единой границе записи, что и гейт #3225, и подключён ко ВСЕМ писателям истории (domclick/detail.py, cian/detail.py, yandex_price_history.py). Критерий требует двух свидетелей: скачок ×10 к предыдущей точке И возврат к базе у следующей; у последней точки серии свидетель — текущая цена объявления, нет и её → точку не трогаем (без второго свидетеля ×10 может быть честной сменой цены). У domklik после выброса пересчитывается diff_percent соседа: парсер считал его от базы, которой больше нет. Миграция 286 чистит уже собранные строки тем же критерием и только у загрузчика (change_time <> recorded_at): триггерные строки — это живые смены listings.price_rub, у них другая база отсчёта (см. 285). Падает, если кандидатов больше 200. |
|||
| d518efbed1 |
fix(tradein/domclick): дом-поля едут в словаре ДОМ.РФ, а не сырьём Домклика
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m26s
houses.material_walls уже заполнена словарём ДОМ.РФ (капремонт КР1.2, #2013): кирпич 2662, железобетонная панель 2107, иное 1850, монолит 754. Ветка писала туда сырую фразу карточки (Монолитный 2656, Кирпичный 2241, Панельный 1671, Монолитно-кирпичный 773) — колонка стала бы двухсловарной, и `WHERE material_walls = 'монолит'` перестал бы видеть весь Домклик. Ровно та болезнь, которую sale_type уже пережил в #2674. canon_wall_type / canon_floor_type стоят на границе записи в houses (как canon_sale_type — на границе записи в listings): Кирпичный→кирпич, Панельный→железобетонная панель, Монолитный/Монолитно-кирпичный→монолит, Блочный/Деревянный→иное, Железобетонный→Железобетонные (форма, уже лежащая в колонке). Незнакомое → None + warning раз на процесс: сырьё в колонку не попадает никогда, а новое значение словаря видно в логах. В raw_payload сырая фраза площадки остаётся как была. Миграция 284 получила тот же CASE lower(...) — иначе backfill залил бы задним числом ровно то, что код перестал писать. CASE без ELSE: незнакомое → NULL. |
|||
| 2532bcbe27 |
fix(tradein/domclick): дом-поля карточки → houses, backfill total_units (#3253)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
Парсер карточки Домклика читал houseInfo.info и складывал блок дома целиком в listings.raw_payload; в houses не переносил ничего. Замер 29.08: total_units = 0 у ВСЕХ источников, хотя quarters_count уже лежал в собранных payload'ах. save_detail_enrichment получает второй оператор — тот же fill-only паттерн, что у avito (#3036), связь через listings.house_id_fk: quarters_count → total_units, wall_type → material_walls, floor_type → material_floors. COALESCE в SET и в WHERE-гейте: непустое значение дома не затирается (у houses есть конкурирующие писатели — ДОМ.РФ капремонт, Houses Catalog). Серия дома, энергоэффективность и число подъездов остаются в raw_payload — колонок под них нет, схему не расширяем. Миграция 284 переливает то же самое задним числом из уже собранных payload'ов, только в пустые колонки, идемпотентно, под lock_timeout. |