chore(tradein): разбор мёртвого кода — подключить, удалить или задокументировать (#2674) #2689

Merged
bot-backend merged 2 commits from chore/2674-dead-code-sweep into main 2026-08-06 00:37:06 +00:00
Collaborator

Summary

Восемь находок раздела «Мёртвый код» разобраны по одному вопросу: механизм рабочий и его некому позвать, или он невыразим/дублирует существующее, или это осознанный задел. Все числа сняты с прод-БД tradein 2026-08-06.

Находка Решение Почему
Скачивание CSV ДОМ.РФ подключить Loader + CLI есть с #2013, но Handler'а и строки расписания не существовало. На проде 29 978 строк staging с ОДНИМ loaded_at (12.07) — ровно один ручной запуск, 24 дня без обновления. Отсюда кормятся houses.year_built/material_walls/total_floorslistings.year_built, когортный фильтр эстиматора
filters_hash подключить Читали estimation.sale.data.filtersHash, Циан кладёт на уровень выше — estimation.sale.filtersHash. Колонка 0/1658, при том что в сохранённых сырых ответах хеш есть у 139/139 и все значения различны
has_panorama подключить Разбирался парсером, лежал в HOUSE_FIELD_PRIORITY, обещан публичным контрактом market.v_houses — и не попадал в houses ни одной строкой кода (0 из 9366)
Дедуп-хелперы оценщика удалить _phys_dedup_key + _extract_street_token: 25 ссылок, все из тестов. Обёртки над живыми _lot_dedup_components / _parse_street_house
Тиерные коэффициенты выкупа удалить asking_to_sold_ratios_tiered (21 строка) + asking_to_sold_tier_bounds (5): ноль читателей, ноль писателей, флага tier_aware_ratio_enabled не существует. computed_at 27.06 — при том что живая asking_to_sold_ratios обновилась 05.08
Колонки без писателя удалить listings.merged_into (93 408 NULL, 0 упоминаний в коде; миграция 113 уже называла её мёртвой) и house_sources.raw_payload (49 502 NULL) вместе с GIN-индексом по всегда-NULL колонке
Метрика расхождения цен удалить показатель, оставить источник v_data_quality.price_disagreements_count — у всех 89 699 объявлений ровно один источник, ноль был структурно неизбежен и читался как «расхождений нет». Сами v_price_divergence / v_cross_source_health оставлены как задел, но подписаны
BROWSER_BLOCK_RESOURCES оставить и задокументировать Переменная задана во всех трёх прод-контейнерах, кода с таким именем нет с #1812

Оборванная проводка, а не мёртвый код

Две находки из восьми оказались рабочими механизмами, которым не хватало соединения.

Загрузчик ДОМ.РФ — самое ценное в списке. Искать надо было не «кто удалил вызов», а «кто должен был вызвать». Планировщик диспетчеризует по паре «строка scrape_schedules» + «Handler в product_handlers»; у ДОМ.РФ не было ни того, ни другого — только CLI, который однажды запустили руками:

domrf_kapremont: 29 978 строк, min(loaded_at) = max(loaded_at) = 2026-07-12 13:19:30

Один и тот же loaded_at у всех строк — подпись единственного прогона. Чинится подключением: Handler + seed-строка с недельным тактом (interval_days: 7), окно 01:00-02:00 UTC — раньше refresh_search_matview (03:00-04:00), импорта ДКП и дневных агрегатов.

filters_hash — не «Циан перестал отдавать», а «читаем не на том уровне». Проверка по сырым ответам, которые уже лежат в БД:

-- ключи estimation.sale на проде
isError | filtersHash | data | isFetching
-- при этом ключи estimation.sale.data:
accuracy | dealType | price | priceFrom | priceSqm | priceTo

filtersHash — сосед data, а не её содержимое. 139/139 строк несут непустой хеш, все 139 различны. Правка на один вызов + бэкфилл из raw_payload.

Про переменную окружения без кода

BROWSER_BLOCK_RESOURCES=true стоит в tradein-scraper, tradein-browser и в окружении сборки. Раскопки по истории: коммит 59b5d157 (#1812) заменил булев выключатель на список типов BROWSER_BLOCK_RESOURCE_TYPES.

Трафик от этого не вырос, и вот почему. До #1812 блокировались image,media,font через page.route. После — font,media тем же route, а image глушится камуфоксом (block_images: True, _launch_browser, безусловно). Покрытие то же; переименовали только ручку. Измерить это ретроспективно по трафику нельзя (нет посуточных счётчиков байтов на контейнер), но по коду видно, что ни один тип ресурса не остался незаблокированным.

Опасность в другом: ручка выглядит рабочей. Оператор, который поставит BROWSER_BLOCK_RESOURCES=false, чтобы посмотреть страницу с ресурсами, ничего не выключит — и сделает вывод про блокировку, а не про переменную. Поэтому сервис теперь предупреждает на старте, а переменная остаётся инертной (реестр _RETIRED_ENV).

Убрать её из окружения — задача devops (compose/.env.runtime вне границ этого PR).

Что оставлено как задел, но подписано

  • v_price_divergence / v_cross_source_health — пусты структурно: боевой путь загрузки (scrapers/base.py::_link_listing_to_house) зовёт upsert_listing_source('source_link') напрямую и не зовёт match_or_create_listing (см. NOTE на matching/listings.py:188). В COMMENT это написано, чтобы «пусто» не читалось как «проверили — чисто».
  • house_sources.ext_url — пуст 49 502/49 502, но входит в публичный контракт market.v_house_sources (мигр. 154), где удаление колонки объявлено ломающим изменением. Оставлен и подписан.
  • listings.canonical — вырожден (t у всех 93 408), но читается предикатом listings_search_mv; снос потянул бы пересоздание matview с шестью индексами ради нулевого выигрыша.

Что найдено попутно и НЕ трогалось

Сканирование pg_stats на null_frac = 1 дало ещё кандидатов, по каждому нужен свой разбор — не смешиваю с этим PR:

  • listings_snapshots.position_in_serp (380 007 NULL) — писатель upsert_listing_snapshot принимает параметр, но ни один вызывающий его не передаёт. Это не мёртвый код, а несоединённый: позиция в выдаче реально доступна на месте скрейпа.
  • scrape_runs.anchor_lat/anchor_lon/radius_m/segment/target_ext_id — 3 216 прогонов, все NULL.
  • deals.cadastral_number/days_on_market/house_type/total_floors/raw_payload — 96 974 NULL.
  • listings_search_mv.district/distance_to_metro_m/last_price_change/photos_count — честные NULL:: placeholder'ы в DDL, но потребитель об этом не знает.
  • Таблица tmp_purged_junk_houses_0702 (3 383 строки) живёт на проде с 2 июля — это мёртвые данные, не код.
  • Отметка времени загрузчика ДОМ.РФ не сдвинется на успешном прогоне, если ДОМ.РФ ничего не опубликовал: loaded_at обновляется только для изменившихся строк (гейт IS DISTINCT FROM в UPSERT). Любой монитор свежести, завязанный на max(loaded_at), объявит здоровый источник протухшим — а такой паттерн в репозитории уже есть. Предсуществующее поведение загрузчика, но актуальным становится именно сейчас, когда мы ставим его на расписание. Заводится отдельной задачей.

Round 2 — правки по ревью

Признак панорамы был недостижим примерно для 10% страниц. Вызов стоял после раннего возврата по пустой истории размещений: страница, отрисованная идеально (год и этажность на месте, гейт выполнен), но без единого объявления в истории, до записи не доходила. Масштаб — 1519 оценок против 1360 домов с историей. Резолв дома и запись панорамы подняты выше возврата; гейт честности не тронут. Цена: match_or_create_house теперь вызывается и для таких страниц (может создать дом) — но это тот же вызов с тем же адресом, который уже отрабатывает на остальных 90%. Дыра была вдобавок закреплена предсуществующим тестом на пустую историю — он переписан и теперь проверяет ровно то, что должен.

Числа в комментариях к схеме были оценками планировщика. reltuples вместо count(*): listings «142 569» против реальных 93 408 (раздув мёртвыми кортежами на 53%), house_sources «46 813» против 49 502. На безопасность удаления это не влияло — нули там точные, — но оценка уезжала в постоянный комментарий к схеме, в PR, тезис которого «каждое утверждение несёт число с прода». Пересчитано точным счётом везде: в шапке миграции, в COMMENT ON COLUMN и здесь.

Окно пересекалось с обновлением поискового представления. Обоснование окна ссылалось на импорт ДКП и дневные агрегаты, но в тех же 03:00-04:00 сидит refresh_search_matview — ровно то задание, которое переносит year_built в поиск (сверено с прод-таблицей scrape_schedules). Планировщик берёт случайный момент внутри окна и гоняет источники параллельно, порядок не гарантирует ничем. Перенесено на 01:00-02:00; в комментарии честно сказано, что гарантии всё равно нет и при аномально долгом прогоне возможно отставание на цикл. Добавлен тест, который ловит откат окна обратно.

Тесты, адресовавшие вызовы по позиции. db.execute.call_args_list[0] в шести чужих тестах — это и была причина, по которой добавление второго execute ломало их разом. Все переведены на фильтр по SQL через общий хелпер _history_rows. То же для side_effect в тесте отката батча: исключение доставалось бы записи панорамы (она свои ошибки глотает), и тест молча проверял бы не тот путь. В test_save_history_items_inserts_each возвращено утверждение о числе коммитов — в первой редакции оно было удалено вместо обновления, тогда как в соседнем файле обновлено; теперь симметрично.

Test plan

  • pytest tradein-mvp/backend — 3521 passed, 9 skipped (deselect тот же, что в CI)
  • Новый tests/test_dead_code_sweep_2674.py — 18 тестов, разбор ниже
  • Фальсификация: все боевые файлы откачены на origin/main → 8/18 краснеют
  • Миграция 216 прогнана на копии прод-схемы (отдельная БД из pg_dump --schema-only, удалена после): применяется чисто, повторное применение идемпотентно, v_data_quality / market.v_house_sources / market.v_houses опрашиваются после
  • ruff check — чисто по изменённым файлам
  • После деплоя: SELECT count(filters_hash) FROM external_valuations → ожидается 139 (было 0)
  • После деплоя: строка domrf_kapremont_load в scrape_schedules, первый прогон в окне 01:00-02:00 UTC следующих суток
  • После первой оценки через yandex_valuation: SELECT count(has_panorama) FROM houses → должно перестать быть нулём

Честно про тесты

Формулировка «18 тестов» звучит сильнее, чем есть. Разбор по тому, что каждый реально ловит (замерено: боевые файлы откачены на origin/main, прогон):

8 настоящих детекторов — краснеют на origin/main: три про запись панорамы (включая новый, на страницы без истории), два про регистрацию Handler'а ДОМ.РФ, один про путь filters_hash, один гейт удалённых дедуп-обёрток, один про предупреждение о мёртвой переменной.

5 тестов только ищут текст в самой миграции (test_domrf_loader_is_seeded_into_schedules, test_domrf_window_does_not_collide_with_matview_refresh, test_filters_hash_backfill_uses_the_same_path, test_migration_drops_exactly_what_was_declared_dead, test_price_divergence_is_documented_as_structurally_empty). Они утверждают, что файл написан так, как написан, — и не поймают миграцию, которая применится грязно или ударит не по тому объекту. Настоящая проверка здесь — прогон на копии прод-схемы, он в Test plan выше.

2 вакуумных до фикса — гейты «НЕ пишем панорамы, когда страница не подтверждена»: до правки не писали вообще, так что они верны и без неё. Смысл появился вместе с записью.

2 гейта против возврата (test_dead_names_absent_from_live_code, test_dropped_columns_have_no_python_writer) — по построению зелёные сейчас, краснеют при регрессии. 1 (test_filters_hash_absent_stays_none) документирует поведение, а не фикс — так и подписан в коде.

Отдельно: tradein-mvp/browser/test_server.py не запускается ни в CI, ни в деплое — pytest ходит только в tradein-mvp/backend. Там же на origin/main уже красный test_pace_provider_disabled_when_zero (проверено на чистой копии main, к этому PR отношения не имеет). Поэтому поведенческие тесты про _warn_retired_env продублированы гейтом по исходнику в backend-наборе.

Refs #2674

## Summary Восемь находок раздела «Мёртвый код» разобраны по одному вопросу: механизм рабочий и его некому позвать, или он невыразим/дублирует существующее, или это осознанный задел. Все числа сняты с прод-БД `tradein` 2026-08-06. | Находка | Решение | Почему | |---|---|---| | Скачивание CSV ДОМ.РФ | **подключить** | Loader + CLI есть с #2013, но Handler'а и строки расписания не существовало. На проде 29 978 строк staging с ОДНИМ `loaded_at` (12.07) — ровно один ручной запуск, 24 дня без обновления. Отсюда кормятся `houses.year_built/material_walls/total_floors` → `listings.year_built`, когортный фильтр эстиматора | | `filters_hash` | **подключить** | Читали `estimation.sale.data.filtersHash`, Циан кладёт на уровень выше — `estimation.sale.filtersHash`. Колонка 0/1658, при том что в сохранённых сырых ответах хеш есть у 139/139 и все значения различны | | `has_panorama` | **подключить** | Разбирался парсером, лежал в `HOUSE_FIELD_PRIORITY`, обещан публичным контрактом `market.v_houses` — и не попадал в `houses` ни одной строкой кода (0 из 9366) | | Дедуп-хелперы оценщика | **удалить** | `_phys_dedup_key` + `_extract_street_token`: 25 ссылок, все из тестов. Обёртки над живыми `_lot_dedup_components` / `_parse_street_house` | | Тиерные коэффициенты выкупа | **удалить** | `asking_to_sold_ratios_tiered` (21 строка) + `asking_to_sold_tier_bounds` (5): ноль читателей, ноль писателей, флага `tier_aware_ratio_enabled` не существует. `computed_at` 27.06 — при том что живая `asking_to_sold_ratios` обновилась 05.08 | | Колонки без писателя | **удалить** | `listings.merged_into` (93 408 NULL, 0 упоминаний в коде; миграция 113 уже называла её мёртвой) и `house_sources.raw_payload` (49 502 NULL) вместе с GIN-индексом по всегда-NULL колонке | | Метрика расхождения цен | **удалить показатель, оставить источник** | `v_data_quality.price_disagreements_count` — у всех 89 699 объявлений ровно один источник, ноль был структурно неизбежен и читался как «расхождений нет». Сами `v_price_divergence` / `v_cross_source_health` оставлены как задел, но подписаны | | `BROWSER_BLOCK_RESOURCES` | **оставить и задокументировать** | Переменная задана во всех трёх прод-контейнерах, кода с таким именем нет с #1812 | ## Оборванная проводка, а не мёртвый код Две находки из восьми оказались рабочими механизмами, которым не хватало соединения. **Загрузчик ДОМ.РФ — самое ценное в списке.** Искать надо было не «кто удалил вызов», а «кто должен был вызвать». Планировщик диспетчеризует по паре «строка `scrape_schedules`» + «Handler в `product_handlers`»; у ДОМ.РФ не было ни того, ни другого — только CLI, который однажды запустили руками: ``` domrf_kapremont: 29 978 строк, min(loaded_at) = max(loaded_at) = 2026-07-12 13:19:30 ``` Один и тот же `loaded_at` у всех строк — подпись единственного прогона. Чинится подключением: Handler + seed-строка с недельным тактом (`interval_days: 7`), окно 01:00-02:00 UTC — раньше `refresh_search_matview` (03:00-04:00), импорта ДКП и дневных агрегатов. **`filters_hash` — не «Циан перестал отдавать», а «читаем не на том уровне».** Проверка по сырым ответам, которые уже лежат в БД: ```sql -- ключи estimation.sale на проде isError | filtersHash | data | isFetching -- при этом ключи estimation.sale.data: accuracy | dealType | price | priceFrom | priceSqm | priceTo ``` `filtersHash` — сосед `data`, а не её содержимое. 139/139 строк несут непустой хеш, все 139 различны. Правка на один вызов + бэкфилл из `raw_payload`. ## Про переменную окружения без кода `BROWSER_BLOCK_RESOURCES=true` стоит в `tradein-scraper`, `tradein-browser` и в окружении сборки. Раскопки по истории: коммит 59b5d157 (#1812) заменил булев выключатель на список типов `BROWSER_BLOCK_RESOURCE_TYPES`. **Трафик от этого не вырос, и вот почему.** До #1812 блокировались `image,media,font` через `page.route`. После — `font,media` тем же route, а `image` глушится камуфоксом (`block_images: True`, `_launch_browser`, безусловно). Покрытие то же; переименовали только ручку. Измерить это ретроспективно по трафику нельзя (нет посуточных счётчиков байтов на контейнер), но по коду видно, что ни один тип ресурса не остался незаблокированным. Опасность в другом: ручка выглядит рабочей. Оператор, который поставит `BROWSER_BLOCK_RESOURCES=false`, чтобы посмотреть страницу с ресурсами, ничего не выключит — и сделает вывод про блокировку, а не про переменную. Поэтому сервис теперь предупреждает на старте, а переменная остаётся инертной (реестр `_RETIRED_ENV`). Убрать её из окружения — задача devops (compose/`.env.runtime` вне границ этого PR). ## Что оставлено как задел, но подписано - `v_price_divergence` / `v_cross_source_health` — пусты **структурно**: боевой путь загрузки (`scrapers/base.py::_link_listing_to_house`) зовёт `upsert_listing_source('source_link')` напрямую и не зовёт `match_or_create_listing` (см. NOTE на `matching/listings.py:188`). В `COMMENT` это написано, чтобы «пусто» не читалось как «проверили — чисто». - `house_sources.ext_url` — пуст 49 502/49 502, но входит в публичный контракт `market.v_house_sources` (мигр. 154), где удаление колонки объявлено ломающим изменением. Оставлен и подписан. - `listings.canonical` — вырожден (`t` у всех 93 408), но читается предикатом `listings_search_mv`; снос потянул бы пересоздание matview с шестью индексами ради нулевого выигрыша. ## Что найдено попутно и НЕ трогалось Сканирование `pg_stats` на `null_frac = 1` дало ещё кандидатов, по каждому нужен свой разбор — не смешиваю с этим PR: - `listings_snapshots.position_in_serp` (380 007 NULL) — писатель `upsert_listing_snapshot` **принимает** параметр, но ни один вызывающий его не передаёт. Это не мёртвый код, а несоединённый: позиция в выдаче реально доступна на месте скрейпа. - `scrape_runs.anchor_lat/anchor_lon/radius_m/segment/target_ext_id` — 3 216 прогонов, все NULL. - `deals.cadastral_number/days_on_market/house_type/total_floors/raw_payload` — 96 974 NULL. - `listings_search_mv.district/distance_to_metro_m/last_price_change/photos_count` — честные `NULL::` placeholder'ы в DDL, но потребитель об этом не знает. - Таблица `tmp_purged_junk_houses_0702` (3 383 строки) живёт на проде с 2 июля — это мёртвые данные, не код. - **Отметка времени загрузчика ДОМ.РФ не сдвинется на успешном прогоне**, если ДОМ.РФ ничего не опубликовал: `loaded_at` обновляется только для изменившихся строк (гейт `IS DISTINCT FROM` в UPSERT). Любой монитор свежести, завязанный на `max(loaded_at)`, объявит здоровый источник протухшим — а такой паттерн в репозитории уже есть. Предсуществующее поведение загрузчика, но актуальным становится именно сейчас, когда мы ставим его на расписание. Заводится отдельной задачей. ## Round 2 — правки по ревью **Признак панорамы был недостижим примерно для 10% страниц.** Вызов стоял после раннего возврата по пустой истории размещений: страница, отрисованная идеально (год и этажность на месте, гейт выполнен), но без единого объявления в истории, до записи **не доходила**. Масштаб — 1519 оценок против 1360 домов с историей. Резолв дома и запись панорамы подняты выше возврата; гейт честности не тронут. Цена: `match_or_create_house` теперь вызывается и для таких страниц (может создать дом) — но это тот же вызов с тем же адресом, который уже отрабатывает на остальных 90%. Дыра была вдобавок закреплена предсуществующим тестом на пустую историю — он переписан и теперь проверяет ровно то, что должен. **Числа в комментариях к схеме были оценками планировщика.** `reltuples` вместо `count(*)`: listings «142 569» против реальных **93 408** (раздув мёртвыми кортежами на 53%), house_sources «46 813» против **49 502**. На безопасность удаления это не влияло — нули там точные, — но оценка уезжала в постоянный комментарий к схеме, в PR, тезис которого «каждое утверждение несёт число с прода». Пересчитано точным счётом везде: в шапке миграции, в `COMMENT ON COLUMN` и здесь. **Окно пересекалось с обновлением поискового представления.** Обоснование окна ссылалось на импорт ДКП и дневные агрегаты, но в тех же 03:00-04:00 сидит `refresh_search_matview` — ровно то задание, которое переносит `year_built` в поиск (сверено с прод-таблицей `scrape_schedules`). Планировщик берёт случайный момент внутри окна и гоняет источники параллельно, порядок не гарантирует ничем. Перенесено на **01:00-02:00**; в комментарии честно сказано, что гарантии всё равно нет и при аномально долгом прогоне возможно отставание на цикл. Добавлен тест, который ловит откат окна обратно. **Тесты, адресовавшие вызовы по позиции.** `db.execute.call_args_list[0]` в шести чужих тестах — это и была причина, по которой добавление второго `execute` ломало их разом. Все переведены на фильтр по SQL через общий хелпер `_history_rows`. То же для `side_effect` в тесте отката батча: исключение доставалось бы записи панорамы (она свои ошибки глотает), и тест молча проверял бы не тот путь. В `test_save_history_items_inserts_each` возвращено утверждение о числе коммитов — в первой редакции оно было удалено вместо обновления, тогда как в соседнем файле обновлено; теперь симметрично. ## Test plan - [x] `pytest tradein-mvp/backend` — 3521 passed, 9 skipped (deselect тот же, что в CI) - [x] Новый `tests/test_dead_code_sweep_2674.py` — 18 тестов, разбор ниже - [x] Фальсификация: все боевые файлы откачены на `origin/main` → 8/18 краснеют - [x] Миграция 216 прогнана на копии прод-схемы (отдельная БД из `pg_dump --schema-only`, удалена после): применяется чисто, повторное применение идемпотентно, `v_data_quality` / `market.v_house_sources` / `market.v_houses` опрашиваются после - [x] `ruff check` — чисто по изменённым файлам - [ ] После деплоя: `SELECT count(filters_hash) FROM external_valuations` → ожидается 139 (было 0) - [ ] После деплоя: строка `domrf_kapremont_load` в `scrape_schedules`, первый прогон в окне 01:00-02:00 UTC следующих суток - [ ] После первой оценки через yandex_valuation: `SELECT count(has_panorama) FROM houses` → должно перестать быть нулём ## Честно про тесты Формулировка «18 тестов» звучит сильнее, чем есть. Разбор по тому, что каждый реально ловит (замерено: боевые файлы откачены на `origin/main`, прогон): **8 настоящих детекторов** — краснеют на `origin/main`: три про запись панорамы (включая новый, на страницы без истории), два про регистрацию Handler'а ДОМ.РФ, один про путь `filters_hash`, один гейт удалённых дедуп-обёрток, один про предупреждение о мёртвой переменной. **5 тестов только ищут текст в самой миграции** (`test_domrf_loader_is_seeded_into_schedules`, `test_domrf_window_does_not_collide_with_matview_refresh`, `test_filters_hash_backfill_uses_the_same_path`, `test_migration_drops_exactly_what_was_declared_dead`, `test_price_divergence_is_documented_as_structurally_empty`). Они утверждают, что файл написан так, как написан, — и **не поймают** миграцию, которая применится грязно или ударит не по тому объекту. Настоящая проверка здесь — прогон на копии прод-схемы, он в Test plan выше. **2 вакуумных до фикса** — гейты «НЕ пишем панорамы, когда страница не подтверждена»: до правки не писали вообще, так что они верны и без неё. Смысл появился вместе с записью. **2 гейта против возврата** (`test_dead_names_absent_from_live_code`, `test_dropped_columns_have_no_python_writer`) — по построению зелёные сейчас, краснеют при регрессии. **1** (`test_filters_hash_absent_stays_none`) документирует поведение, а не фикс — так и подписан в коде. Отдельно: `tradein-mvp/browser/test_server.py` **не запускается ни в CI, ни в деплое** — pytest ходит только в `tradein-mvp/backend`. Там же на `origin/main` уже красный `test_pace_provider_disabled_when_zero` (проверено на чистой копии main, к этому PR отношения не имеет). Поэтому поведенческие тесты про `_warn_retired_env` продублированы гейтом по исходнику в backend-наборе. Refs #2674
bot-backend added 1 commit 2026-08-06 00:03:40 +00:00
chore(tradein): разбор мёртвого кода — подключить, удалить или задокументировать (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 2m57s
f5b39e6fc9
Восемь находок «написано, покрыто тестами, ни разу не сработало» разведены на три
разных диагноза. Две из восьми оказались не мёртвым кодом, а оборванной проводкой.

ПОДКЛЮЧЕНО

Загрузчик ДОМ.РФ. Loader и CLI существуют с #2013, а Handler'а в product_handlers
и строки в scrape_schedules не было — вызвать его было нечем. На проде 29 978 строк
staging с ОДНИМ loaded_at (2026-07-12), то есть ровно один ручной запуск, 24 дня
без обновления. Отсюда кормятся houses.year_built/material_walls/total_floors и
дальше listings.year_built — когортный фильтр эстиматора. Недельный такт, окно
03:00-04:00 UTC (до импорта ДКП и дневных агрегатов).

filters_hash. Парсер читал estimation.sale.data.filtersHash, а Циан кладёт ключ
уровнем выше — estimation.sale.filtersHash. Колонка пуста 0/1658, при том что в
сохранённых сырых ответах хеш есть у 139/139 и все значения различны. Путь исправлен,
139 строк восстановлены бэкфиллом из raw_payload.

has_panorama. Разбирался парсером, лежал в карте приоритетов, обещан публичным
контрактом market.v_houses — и не попадал в houses ни одной строкой кода (0 из 9366).
Пишется там, где yandex_valuation уже держит и house_id, и мету. Гейт честности:
парсер отдаёт bool, а не bool|None, поэтому false пишем только при подтверждённо
отрисованной странице (есть год или этажность) — иначе NULL, а не выдуманный false.

УДАЛЕНО

Дедуп-обёртки эстиматора _phys_dedup_key / _extract_street_token: 25 ссылок, все из
тестов. Хуже, чем просто мёртвые — _phys_dedup_key утверждала правило «ключ = кадастр
ИЛИ улица», которого в боевом дедупе нет (_union_find_phys_dedup держит оба композита
и сливает по любому совпадению). Тесты переведены на живые функции.

Тиерные коэффициенты выкупа asking_to_sold_ratios_tiered + asking_to_sold_tier_bounds:
ноль читателей и писателей, флага tier_aware_ratio_enabled не существует. Посчитаны
один раз при накатке 098 (computed_at 2026-06-27) — тогда как живая
asking_to_sold_ratios обновляется ежедневно (2026-08-05). Методика сохранена в 098.

Колонки без писателя: listings.merged_into (113 уже называла её мёртвой) и
house_sources.raw_payload вместе с GIN-индексом по всегда-NULL колонке.

v_data_quality.price_disagreements_count: у всех 89 699 объявлений ровно один
источник, показатель структурно не мог быть ненулевым, а ноль читался как
«расхождений нет».

ЗАДОКУМЕНТИРОВАНО

BROWSER_BLOCK_RESOURCES выставлен во всех трёх прод-контейнерах, а код перестал его
читать в #1812. Блокировка при этом не ослабла (image глушит camoufox block_images,
font/media — дефолт списка типов), мёртв только выключатель. Сервис теперь говорит
об этом на старте: молча игнорируемая ручка опаснее отсутствующей.

v_price_divergence / v_cross_source_health оставлены как задел, но в COMMENT написано,
почему они пусты структурно: боевой путь загрузки зовёт upsert_listing_source
напрямую и не зовёт match_or_create_listing.

house_sources.ext_url пуст 46 813/46 813, но входит в публичный контракт
market.v_house_sources — оставлен и подписан.

Refs #2674
Light1YT added 1 commit 2026-08-06 00:32:24 +00:00
fix(tradein): панорама для страниц без истории, точные счётчики, окно без гонки (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
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 3m0s
3d38d589d0
Правки по ревью PR #2689.

Признак панорамы был недостижим примерно для десятой части страниц. Вызов стоял
после раннего возврата по пустой истории размещений, поэтому идеально отрисованная
страница без единого объявления до записи не доходила: на проде 1519 оценок против
1360 домов с историей. Резолв дома и запись панорамы подняты выше возврата — гейт
честности не тронут. Цена: match_or_create_house теперь вызывается и для таких
страниц (может создать дом), но это тот же вызов с тем же адресом, который уже
отрабатывает на остальных 90%.

Числа в комментариях к схеме были оценками планировщика, а не точным счётом:
listings 142 569 против реальных 93 408 (раздув мёртвыми кортежами на 53%),
house_sources 46 813 против 49 502. На безопасность удаления это не влияло — нули
там точные, — но оценка уезжала в постоянный комментарий к схеме, в PR, тезис
которого «каждое утверждение несёт число с прода». Пересчитано точным count(*).

Окно расписания ДОМ.РФ 03:00-04:00 совпадало с refresh_search_matview — то есть
ровно с тем заданием, которое переносит year_built в поиск. Планировщик берёт
случайный момент внутри окна и гоняет источники параллельно, так что порядок был
подбрасыванием монеты. Перенесено на 01:00-02:00; в комментарии честно сказано, что
гарантии всё равно нет и при аномально долгом прогоне возможно отставание на цикл.

Тесты, адресовавшие вызовы по позиции (db.execute.call_args_list[0]), переведены на
фильтр по SQL — это и была причина, по которой добавление второго execute ломало
шесть чужих тестов разом. То же для side_effect в тесте отката батча: исключение
доставалось бы записи панорамы, которая свои ошибки глотает, и тест молча проверял
бы не тот путь. В test_save_history_items_inserts_each возвращено утверждение о
числе коммитов (было удалено вместо обновления).

Refs #2674
bot-backend merged commit fb5ec56a54 into main 2026-08-06 00:37:06 +00:00
Author
Collaborator

Отложенная проверка закрыта: признак панорамы доехал и работает

При мерже третий пункт плана проверки остался в состоянии «не проверено»: код доехал, но yandex_valuation с момента деплоя ни разу не бежал, а писать признак больше некому.

Сейчас прогон состоялся:

external_valuations, yandex_valuation:  1 519 → 1 520   (последняя 2026-08-06 06:28:46, деплой был в 00:44)
houses.has_panorama непустых:               0 → 1

Одна новая оценка — один заполненный признак. Ровно то, чего ждали: поле парсилось и покрывалось тестами, но в базу не попадало ни одной строкой.

Заодно закрывается и вопрос о том, была ли правка достижима для страниц без истории размещения: вызов был перенесён выше раннего возврата именно ради тех ~10%, и первая же запись это подтверждает лишь частично — одного наблюдения мало, чтобы судить о доле. Полная картина появится по мере накопления оценок.

Статус остальных двух пунктов не менялся:

  • хеш фильтров Циана — бэкфилл отработал (0 → 139 из 139), но новых строк не появится: cian_valuation мёртв с 29 июня из-за протухших кук (#2700), проверить парсер на живом ответе нельзя;
  • расписание загрузки ДОМ.РФ — доехало сразу, next_run_at в окне 01:00–02:00 UTC.
## Отложенная проверка закрыта: признак панорамы доехал и работает При мерже третий пункт плана проверки остался в состоянии «не проверено»: код доехал, но `yandex_valuation` с момента деплоя ни разу не бежал, а писать признак больше некому. Сейчас прогон состоялся: ``` external_valuations, yandex_valuation: 1 519 → 1 520 (последняя 2026-08-06 06:28:46, деплой был в 00:44) houses.has_panorama непустых: 0 → 1 ``` Одна новая оценка — один заполненный признак. Ровно то, чего ждали: поле парсилось и покрывалось тестами, но в базу не попадало ни одной строкой. Заодно закрывается и вопрос о том, была ли правка достижима для страниц без истории размещения: вызов был перенесён выше раннего возврата именно ради тех ~10%, и первая же запись это подтверждает лишь частично — одного наблюдения мало, чтобы судить о доле. Полная картина появится по мере накопления оценок. Статус остальных двух пунктов не менялся: - **хеш фильтров Циана** — бэкфилл отработал (0 → 139 из 139), но новых строк не появится: `cian_valuation` мёртв с 29 июня из-за протухших кук (#2700), проверить парсер на живом ответе нельзя; - **расписание загрузки ДОМ.РФ** — доехало сразу, `next_run_at` в окне 01:00–02:00 UTC.
Author
Collaborator

Загрузка ДОМ.РФ отработала по расписанию — впервые за историю продукта

Диагноз при постановке был такой: механизм написан и покрыт тестами, но не имеет ни строки расписания, ни обработчика, и вызывался только из командной строки. Признак, по которому это было опознано, — одинаковая метка времени у всех 29 978 строк, то есть единственный ручной запуск.

Сегодня в 01:00:59 расписание, засеянное миграцией 216, сработало впервые:

scrape_runs, domrf_kapremont_load:  ровно ОДНА строка за всю историю — сегодняшняя
  {"kr11_rows": 29978, "total_seen": 29978, "new_count": 1087,
   "upserted": 11, "houses_updated": 12, "listings_updated": 1075}

domrf_kapremont:  29 978 строк · 2 различные метки загрузки
  2026-07-12 13:19:30   ← тот самый единственный ручной запуск
  2026-08-07 01:01:03   ← первый запуск по расписанию

Диагностический признак подтвердился с обратной стороны. Он был поставлен по наблюдению «одна метка на всю таблицу»; теперь меток стало две, и вторая — ровно первый автоматический запуск. Если бы находка была неверна (например, механизм на самом деле как-то вызывался), второй метки не появилось бы, а появилась бы серия.

Работа сделана настоящая, а не холостая: 1 087 новых записей, обновлено 12 домов и 1 075 объявлений.

Почему это стоит отметить отдельно

Из трёх диагнозов, которые эпик различает — «оборванная проводка», «мёртвый код», «невыразимый механизм», — этот был отнесён к первому. Диагноз определял действие: проводку чинят (дёшево, возвращает готовую работу), мёртвый код удаляют, невыразимое документируют и убирают.

Ошибись мы здесь в сторону «мёртвого кода» — удалили бы рабочий загрузчик вместе с тестами. Ошибись в сторону «невыразимого» — оставили бы как есть навсегда.

Проверка стоила одной строки расписания и подтвердилась через сутки фактом, а не рассуждением.

Оговорка

Один успешный прогон не доказывает устойчивости. Такт недельный, следующий — 14.08; тогда и станет видно, держится ли. До тех пор корректная формулировка: «сработало один раз, как задумано».

## Загрузка ДОМ.РФ отработала по расписанию — впервые за историю продукта Диагноз при постановке был такой: механизм написан и покрыт тестами, но **не имеет ни строки расписания, ни обработчика**, и вызывался только из командной строки. Признак, по которому это было опознано, — **одинаковая метка времени у всех 29 978 строк**, то есть единственный ручной запуск. Сегодня в 01:00:59 расписание, засеянное миграцией 216, сработало впервые: ``` scrape_runs, domrf_kapremont_load: ровно ОДНА строка за всю историю — сегодняшняя {"kr11_rows": 29978, "total_seen": 29978, "new_count": 1087, "upserted": 11, "houses_updated": 12, "listings_updated": 1075} domrf_kapremont: 29 978 строк · 2 различные метки загрузки 2026-07-12 13:19:30 ← тот самый единственный ручной запуск 2026-08-07 01:01:03 ← первый запуск по расписанию ``` **Диагностический признак подтвердился с обратной стороны.** Он был поставлен по наблюдению «одна метка на всю таблицу»; теперь меток стало две, и вторая — ровно первый автоматический запуск. Если бы находка была неверна (например, механизм на самом деле как-то вызывался), второй метки не появилось бы, а появилась бы серия. Работа сделана настоящая, а не холостая: 1 087 новых записей, обновлено 12 домов и 1 075 объявлений. ## Почему это стоит отметить отдельно Из трёх диагнозов, которые эпик различает — «оборванная проводка», «мёртвый код», «невыразимый механизм», — этот был отнесён к первому. Диагноз определял действие: проводку **чинят** (дёшево, возвращает готовую работу), мёртвый код **удаляют**, невыразимое **документируют и убирают**. Ошибись мы здесь в сторону «мёртвого кода» — удалили бы рабочий загрузчик вместе с тестами. Ошибись в сторону «невыразимого» — оставили бы как есть навсегда. Проверка стоила одной строки расписания и подтвердилась через сутки фактом, а не рассуждением. ## Оговорка Один успешный прогон не доказывает устойчивости. Такт недельный, следующий — 14.08; тогда и станет видно, держится ли. До тех пор корректная формулировка: «сработало один раз, как задумано».
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2689
No description provided.