Правки по ревью 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
Восемь находок «написано, покрыто тестами, ни разу не сработало» разведены на три
разных диагноза. Две из восьми оказались не мёртвым кодом, а оборванной проводкой.
ПОДКЛЮЧЕНО
Загрузчик ДОМ.РФ. 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
Group E4 (final, highest-risk step of scraper_kit migration epic #2277):
estimator.py and house_imv_backfill.py's avito_imv/cian_valuation/
yandex_valuation call sites now import from scraper_kit.providers.* instead
of app.services.scrapers.*, following the exact wiring proven safe by E1/E2/E3
(#2334/#2335/#2336):
- estimator.py's avito IMV (_get_or_fetch_imv_cached, both call sites) and
house_imv_backfill.py's _process_one_house: add config=RealScraperConfig()
— kit's evaluate_via_imv silently drops the configured proxy without it.
- estimator.py's cian valuation (Stage 9): add config=RealScraperConfig() —
mandatory kwarg on the kit function (TypeError if omitted).
- estimator.py's yandex valuation: add config=RealScraperConfig() (mandatory)
and delay_provider=get_scraper_delay — without it kit silently falls back
to a hardcoded 5.0s throttle instead of the DB-configured anti-ban delay.
- All exception classes imported consistently from the same kit module as
evaluate_via_imv (not just the function) — mixing legacy/kit exception
classes would break `except IMVAddressNotFoundError` etc. via identity
mismatch (caught by an existing test that assumed the legacy class,
fixed alongside).
Observability: both cian_valuation and yandex_valuation graceful-degradation
except-blocks upgraded from logger.warning to logger.exception. GlitchTip's
LoggingIntegration listens at event_level=ERROR (main.py/scheduler_main.py) —
a WARNING never reaches GlitchTip as an event regardless of exc_info, so a
future config-wiring mistake at these call sites needs ERROR level to be
visible in monitoring.
house_imv_backfill.py: RealScraperConfig is imported lazily inside
_process_one_house (not at module level) to avoid a circular import —
app.services.scraper_adapters imports backfill_house_imv/
process_houses_imv_batch from this module at module level. Verified via
direct import in both orders plus a full `app.main` import.
Also fixes a stale docstring claiming process_houses_imv_batch is "not wired
into scheduler" — it is, via scrape_pipeline.py's run_avito_city_sweep.
Test updates: 2 pre-existing tests (test_backfill_wave2.py) mocked the legacy
IMVAddressNotFoundError/IMVEvaluation/IMVGeo classes, now updated to import
from scraper_kit to match the production exception identity. Added
config=/delay_provider= regression-guard asserts to the relevant estimator
and backfill tests, mirroring the existing #2306 cian_price_history pattern.
Legacy app/services/scrapers/{avito_imv,cian_valuation,yandex_valuation}.py
are untouched and still imported by cian_history_backfill.py's valuation
block (separately scoped, not touched here) — revert is a clean single-commit
revert, no schema/data migration involved.