_PRODUCT_SOURCES в test_scraper_kit_scheduler_parity.py — ручной canonical-список
(файл сам предписывает обновлять его при изменении product_handlers.py, #2397 Part C
уже чинил ровно эту протухшесть). Новый source из PR #2579 забыт — без него
test_real_build_product_handlers_covers_all_scheduled_sources не покрывает
регистрацию Handler'а: пропажа entry в product_handlers.py осталась бы незамечена,
а kit-scheduler логировал бы "unknown source, skip" каждый тик без выполнения задачи.
Проверено: тест падает при временном откате Handler-registration (AssertionError
misses source=deal_city_price_bands_refresh), проходит после восстановления.
Миграция 178 засеяла deal_city_price_bands разово (N>=30 сделок), без scheduler'а
на refresh. Город без строки падал на глобальный DEAL_MIN_PPM2=50000 (ЕКБ-калибровка)
— для дешёвых городов области это не anti-outlier guard, а cut-off легитимного рынка
(Североуральск median ~21.7k). Замер по прод-данным: 289 из 369 не-ЕКБ городов
(1265 сделок) не имели строки и падали на ЕКБ-порог.
- 194_deal_city_price_bands_tiers.sql — трёхуровневая схема (tier колонка):
full (N>=30, own p1/p99, unchanged) / rough (N 10-29, own p1 floor + фикс.
ceiling 800000) / region_fallback (N 1-9, pooled областной p1=15263 вместо
ЕКБ-порога). Екатеринбург по-прежнему не в таблице — estimator fallback
byte-identical.
- 195_scrape_schedules_seed_deal_city_price_bands_refresh.sql — scrape_schedules
row, окно 07:00-08:00 UTC (после rosreestr_dkp_import + asking_to_sold_ratio_refresh).
- app/tasks/deal_city_price_bands_refresh.py — периодический re-derive (kit-scheduler,
byte-identical 194 derivation), без DELETE (множество городов монотонно растёт).
- app/services/product_handlers.py — регистрация Handler для нового source.
Валидация: scratch-БД (syntax_check) в прод-контейнере, synthetic данные на
границах тиров (N=9/10/29/30) + Екатеринбург/non-rosreestr/NULL exclusion, оба
файла применены дважды (идемпотентность подтверждена), scratch-БД удалена.