После удаления legacy-пайплайна эти сценарии были заассерчены только у avito
(test_3319) и дрейн до первого якоря у yandex/cian (test_3333). Добавлено:
- таймаут якоря у cian и yandex: якорь не в чекпоинте, следующий обработан,
errors_count растёт; таймаут фазы у domclick: завершённые корзины в чекпоинте,
статус failed, а не done;
- SIGTERM-дрейн после первого якоря у cian и yandex: interrupted=1, в
чекпоинте ровно первый якорь, второй не посещён;
- пользовательская отмена (is_cancelled=True) после первого якоря у avito,
cian и yandex: второй якорь не посещён, чекпоинт на месте, без mark_done.
Проверка по строке прогона: фейковая БД мержит counters всех записей, как
jsonb-мерж в runs.py. Мутации (снятый continue/return/interrupted/errors_count
в pipeline.py) роняют ровно свой тест, 6 из 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fetch_newbuilding, resolve_cian_zhk_url_via_search, YandexNewbuildingScraper и
resolve_yandex_jk_slug принимали config=None и в этом случае строили
BrowserFetcher(endpoint=None) мимо фабрики, а резолвер Циана — сессию без прокси.
Все боевые вызовы config уже передают, но новый вызывающий без config молча ушёл бы
на env-узел сайдкара мимо пула — именно так потеряли 8 суток в #2767 и сломались
в #3197. Теперь config обязателен по сигнатуре, запасные ветки удалены, фетчер
строит только build_browser_fetcher.
Тесты: кейсы *_without_config (ждали endpoint=None) заменены одним — без config
TypeError до сети, фетчер и сессия не строятся; остальные тестовые вызовы
получили config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прод: 7193, 7218, 7264, 7302 (15-16.09) и 6608/6631/6643/6662 (10.09) — по
1600 попыток, 1-10 карточек, ~1599 блоков, status='done', по 70+ минут
каждый. В те же окна в логах tradein-browser шторм «browser launch failed …
InvalidIP» и «/fetch 503». 6885 (13.09) — те же 1600 попыток при
ban_kinds {"infra": 1591}, но 'banned'.
Корень — регресс #3288/#3367: record_block(kind != platform) уходит в
record_failure(), то есть только в знаменатель окна. Числитель доли растёт
лишь на platform, safety-net смотрит на platform-серию, счётчика серии
infra нет вовсе. Серия отказов сайдкара любой длины не рвала прогон ничем,
а финализатор #3367 при одной карточке ставил 'done'.
- BlockRatioBreaker: infra_streak_limit и счётчик серии infra. Серию гасит
любой ответ площадки — успех, её блок, 404. abort_reason() -> "infra_streak"
(проверяется первым), abort_explanation() называет эту величину.
- avito_detail_backfill: порог = max_consecutive_failures (дефолт 25) — тот же
разряд «отказ нашей стороны», что и отказы-не-блоки. На обрыве — mark_failed
«тракт недоступен: N отказов … подряд без единого ответа площадки», в
counters abort_reason=infra_streak и ban_kinds. Не 'banned' — площадка
молчала; не через mark_backfill_finished — там одна карточка даёт 'done'.
Тесты по значению: 1 успех + 1599 отказов сайдкара -> attempted=26,
mark_failed, abort_reason=infra_streak, ban_kinds {"infra": 25}; обрыв ровно
на 25-м (24 — нет); 24 infra + ответ площадки (успех/блок/404) + 24 — не
обрыв; 24 infra + ответ, трижды — снапшот пройден; 25% настоящих блоков —
не обрыв; прежний контроль #3367 (20 infra + 10 успехов -> done) зелёный.
Фальсификация: без проводки порога — «assert (1600, 1600) == (26, 26)»; без
сброса серии на блоке площадки — «abort_reason='infra_streak': 24 + ответ
площадки (platform) + 24»; без ветки финала — «прогон ушёл в
backfill-honest-status: там 1 карточка превращает шторм в 'done'».
Пункт 3 задачи (browser_unfit узлу, на котором сайдкар не поднял браузер)
не сделан: это вердикт пула прокси и протокол сайдкара, отдельная задача.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Виновник по живому снимку pg_stat_activity 17.09 07:48 UTC: pid 1933154 из
tradein-scraper, 'idle in transaction' 1 ч 35 мин, последний запрос
`SELECT status FROM scrape_runs WHERE id = $1` (runs.is_cancelled),
xact_start через 30 мс после старта domclick_city_sweep_moskva 7344.
По Prometheus за 03-17.09 10 из 12 окон «транзакция > 1 ч» совпадают по
началу и концу со свипами ДомКлика.
is_cancelled делал SELECT без commit, а вызывающие сразу уходят в сетевую
фазу (у ДомКлика — весь fetch_city, до 3 ч). Теперь commit после чтения —
в общей точке для всех 12 вызовов; незакоммиченных записей, рассчитанных на
откат, перед вызовами нет (перед каждым — update_heartbeat/save_listings с
commit или чтения).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Выдача Яндекса капчит конкретные выходные узлы пула (замер владельца 29.08:
узлы 1/10/11 — капча, 9 и домашний IP — выдача), поэтому путь через поиск
срабатывал ~2 раза из 9 и стоил ~15 с на подъём якоря. В логах tradein-browser
15-16.09 все 7 попыток — капча, и все 7 через 0,2 с заканчивались «якорная
вкладка не поднялась (Error)». Referer карточке и так даёт переход с выдачи
площадки (detail.py: referer=origin).
Дефолт BROWSER_ANCHOR_VIA_SEARCH теперь пустой: путь через поиск остаётся,
но только по явному env. Тесты яндекс-пути включают его явно; новый тест
грузит модуль без env и проверяет, что якорь идёт прямым goto(origin).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пункт 5 из комментария 05.09: ban_kind пишется только у banned-прогонов, у
done/failed диагноза в БД нет. Прод: 7193, 7218, 7264, 7302 — по 1600
попыток, ~1599 блоков, status='done', counters без ban_kinds; что это были
отказы сайдкара, а не площадки, видно лишь по косвенному признаку (мелкая
block_streak_histogram считает только platform-серии). mark_backfill_finished
кладёт перепись в counters только на ветке бана.
Задача теперь кладёт counters.ban_kinds сама перед любым финализатором
(mark_backfill_finished / mark_failed пустого пула). Статусы и колонка
ban_kind не тронуты — контракты #3196/#2764 как были.
Тест по значению: 20 отказов сайдкара + 10 успехов — counters, уходящие в
финализатор, несут {"infra": 20}. Фальсификация: без строки —
«assert None == {'infra': 20}».
Стоп-кран для infra-серии (вторая половина п.5) — отдельным коммитом #3439.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прод 17.09, printenv: AVITO_DETAIL_BACKFILL_USE_CURL=false у tradein-scraper
и true у tradein-backend/tgbot/browser. Источник true — хостовый
backend/.env.runtime:37; false задан только environment'ом сервиса scraper.
tradein-backend исполняет добор сам (ручка админки
scrape_avito_detail_backfill), то есть ручной прогон шёл curl'ом, который
QRATOR PoW не проходит.
- docker-compose.prod.yml: AVITO_DETAIL_BACKFILL_USE_CURL "false" у backend
и tgbot — у всех сервисов образа backend одно значение (environment
перекрывает env_file). tgbot добор не исполняет; строка там, чтобы правило
«одно значение на образ» проверялось без списка исключений.
- Остальные флаги режима проверены на проде: SCRAPER_FETCH_MODE=browser и
AVITO_IMV_USE_BROWSER_FETCHER=true одинаковы во всех четырёх контейнерах.
- yandex_cookies_file удалён из Settings: его не читал ни один модуль
(только мок в test_scraper_proxy.py), файла /app/yandex_cookies.json нет
ни в одном контейнере, куки Яндекса давно живут в БД (#3192).
extra="ignore" держит старт при оставшейся переменной в .env.runtime.
Тест по значению: разобранный compose — у каждого сервиса образа backend
значение "false"; Settings с YANDEX_COOKIES_FILE в env стартует без поля.
Фальсификация: без строки у backend — «{'backend': None} != {'backend':
'false'}»; с возвращённым полем — «assert not True».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
camoufox 0.5.5 при geoip=True кладёт координаты из GeoIP-записи exit-IP
безусловно, а timezone — через setdefault, поэтому ручной
config["timezone"]="Europe/Moscow" перебивал зону из IP. Узлы пула 13 и 15
(T2 NSK, UTC+7) и 14 (МегаФон Самара, UTC+4, RIPE 17.09) заявляли МСК при
сибирских/самарских координатах, а библиотека писала LeakWarning на каждом
запуске (4715 штук за 13 ч при 665 запусках). Ротация узла идёт внутри
подсети оператора, смена узла релончит браузер — зона с запуска не протухает.
Тест: в kwargs запуска нет ключей timezone/geolocation:* в config, geoip=True.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cian_city_sweep 6179 (06.09) — failed по «фаза houses отказала полностью, 30 из
30», но в done_buckets все пять якорей. 6276 резюмировался от него, пропустил
все якоря и отчитался done с houses_attempted=0, lots_fetched=0.
Отметка «якорь пройден» опиралась только на поток управления, а фаза отказывает
и без исключения (fetch_newbuilding -> None, растёт счётчик). Гейт
_bucket_phase_totally_failed сверяет прирост пар X_attempted/X_failed за якорь;
порог одна попытка. Тот же гейт в run_avito_city_sweep (detail). Ветка «houses
DB query failed» теперь растит houses_attempted вместе с houses_failed.
Форма чекпоинта прежняя (плоский список имён).
Перенос ветки fix/3415-bucket-done-only-if-phases-ok (19a335f4) на свежий main,
применилась без конфликтов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
curl_cffi не проходит QRATOR proof-of-work Авито в принципе (замер 21.08:
~6% обогащений против ~74% на браузере). Шаг 3 задачи был сделан только
environment'ом сервиса scraper, дефолт в config.py остался True. Добор при
этом исполняет и tradein-backend — ручка админки scrape_avito_detail_backfill
вызывает run_avito_detail_backfill в своём процессе, и там printenv даёт true.
Ручной прогон через админку шёл curl'ом.
Дефолт avito_detail_backfill_use_curl -> False. Комментарий в задаче «true
только у tradein-backend, который добор не запускает» был неверен — исправлен,
как и два упоминания use_curl=True «прод-дефолтом».
Тест по значению: Settings без env и без .env даёт False; контроль — env
по-прежнему переключает на curl. Фальсификация: default=True ->
«AssertionError: assert True is False».
На проде хостовый backend/.env.runtime:37 задаёт true явно, поэтому для
tradein-backend нужен ещё environment-оверрайд — он в коммите #3182.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Исключение, вылетевшее из хендлера до его try с mark_failed (прод: пустой пул
прокси в BrowserFetcher.__aenter__ через 10 мс после claim), планировщик только
логировал. Строка оставалась 'running' с heartbeat_at == started_at, reaper через
6 ч ставил 'zombie' — за 21 сутки так 18 avito-свипов (7349 и 7350 висят сейчас).
_dispatch._run теперь финализирует прогон сам: пустой пул прокси в цепочке
причин -> banned/ban_kind=infra (как в пайплайнах), остальное -> failed.
Уже финализированную хендлером строку не трогает (WHERE status='running'),
пустые counters мержатся и чекпоинт не стирают.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Синхронная пересылка алерта была ограничена таймаутом одного HTTP-запроса
(8 с), а попыток после ретранслятора (#3471) больше одной: relay, прямой путь,
пауза 2 с, повтор. 16.09.2026 16:42 UTC отказ шёл медленно, GlitchTip
(aiohttp ClientTimeout total=10) оборвал запрос на 10-й секунде, Caddy записал
status=0, а хендлер досчитал 200 уже разорванному клиенту: uvicorn такой ответ
выбрасывает вместе со строкой access-log. Исход у отправителя и приёмника
расходился, 502 и фоновая доставка не срабатывали.
Вся синхронная попытка теперь под asyncio.timeout(7 с). TimeoutError идёт тем
же путём, что TelegramError: logger.exception, 502, фоновая доставка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Пять булевых флагов кластера сняты ещё в #2475. Оставшиеся 14 числовых полей
Settings перенесены в estimator.py константами с теми же значениями:
IMV_BLEND_WEIGHT 0.5, IMV_BLEND_THRESHOLD 1.15, SB_MIN_COMPS 4, SB_AREA_SIGMA
0.18, SB_ROOMS_MATCH_BOOST 1.6, SB_FLOOR_SIGMA 0.25, SB_GUARDRAIL_TOL 0.05,
SB_MAD_K 3.5, SB_MAD_K_SMALL_N 2.5, SB_SMALL_N_THRESHOLD 10,
ANCHOR_TIER_C_CORRIDOR_MULT 1.5, FSD_K 1.65, SB_GATE_MIN_N 3, SB_GATE_MAX_FSD
0.20. На проде 17.09 все 14 равны дефолтам, ENV-оверрайдов нет. Мёртвая
проверка `tier_c_mult > 0` (константа против нуля) убрана.
Тесты подменяют SB_MIN_COMPS на модуле вместо поля settings. Реплей бэктеста
по сделкам побитово тот же, срабатываний якоря 985, IMV-blend 3, low-conf
гейта 11 — как на main. Шесть порогов (rooms_boost, floor_sigma,
guardrail_tol, tier_c_mult, fsd_k, gate_max_fsd) не ловит ни один
поведенческий тест и ни гейт, их держит только тест боевых значений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Кламп headline к коридору ДКП выключается не только малым числом сделок
(advisory_only, #3452), но и якорем Tier A: _apply_corridor_clamp его exempt,
radius-floor требует anchor_tier is None. Ревьюер #3462 воспроизвёл n=20,
advisory_only=False, headline 202 100 против потолка 140 000 — подписи нет.
- Признак — analog_tier == "same_building" (уже структурный в POST).
corridorAdvisoryNote принимает тир и при same_building говорит
«справочно: цена посчитана по аналогам в этом же доме — коридор её не
ограничивает»; оба вызова (v1 HeroSummary, v2 mappers) передают тир.
- GET-rehydrate терял analog_tier (колонки нет) — якорный тир теперь
восстанавливается из подписи якорного блока в confidence_explanation
через общую константу (analog_tier_from_explanation), как радиус в #2632.
Разбор по всей фразе: радиусный тир S пишет «(аналоги из того же дома)»,
на проде таких строк 4.
- Тесты floor не доходили до floor: три лота уводили в #oblast-E, headline
брался из медианы коридора. На main с полностью выключенным floor файл
зелёный. Лотов шесть, ожидания точные, добавлен кейс n = min_n − 1.
- Полоса маркера corridor_advisory_zone — n = 1..9, не 3..9: уличный коридор
отдаётся с одной сделки. Формулировки поправлены, тест на n=2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Флаг estimate_expected_sold_le_asking снят: оба клампа (ratio > 1.0 и
повторный после хедоники) безусловные. На проде 17.09 флаг = True во всех
трёх контейнерах, ENV-оверрайда нет, фикстура бэктеста захвачена с True.
Флаги corridor_clamp/radius_floor enabled сняты ещё в #2475.
Числовые пороги перенесены с прежними значениями: CORRIDOR_CLAMP_SLACK 0.40,
RADIUS_FLOOR_FACTOR 0.8, OUTLIER_SMALL_N_THRESHOLD 15, OUTLIER_TUKEY_K_SMALL 1.0
— в estimator.py; CORRIDOR_CLAMP_MIN_N 10 — в app.core.config рядом с
LISTINGS_FRESH_DAYS, потому что его же читает DkpCorridor.advisory_only (#3452),
а схема не должна тянуть estimator. Мёртвая проверка `tukey_k_small < 1.5`
(константа против константы) убрана. Сегментный множитель (#2255) не тронут,
порядок операций прежний.
Тесты: OFF-тесты клампа удалены; тест потолка хедоники берёт ratio 0.70, при
котором кламп не срабатывает (0.70 × 1.30 = 0.91), вместо выключения клампа.
Реплей бэктеста по сделкам побитово тот же (бизнес 169, элит 5, премиум 4).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Единственный переключатель estimate_quarter_index_enabled снят ещё в #2475.
Оставшиеся шесть числовых полей Settings (min_n_deals 10, match_skip_ratio 0.6,
max_for_small_n 2.0, small_n_threshold 50, factor_min 0.6, factor_max 1.8)
перенесены в estimator.py константами QUARTER_* с теми же значениями и
комментариями #764/#859; бэктест берёт QUARTER_INDEX_MIN_N_DEALS из модуля.
На проде 17.09 все шесть равны дефолтам, ENV-оверрайдов нет.
Замороженная фикстура квартальный индекс не применяет ни разу (0 из 1600
сделок), а поведенческие тесты не краснеют при подмене трёх порогов из шести,
поэтому добавлен tests/test_1970_estimator_constants.py: боевые значения,
снятые с прода, и проверка, что поле не вернулось в Settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Флаг estimate_dedup_analogs_enabled снят: кросс-source дедуп работает всегда.
На проде 17.09 флаг = True во всех трёх контейнерах (backend/scraper/tgbot),
ENV-оверрайда нет; фикстура бэктеста захвачена с True, поэтому пин флага в
реплее и monkeypatch в гейте больше не нужны.
Числовые пороги estimate_wide_corridor_threshold и три
estimate_manual_review_* перенесены в estimator.py константами модуля с
прежними значениями (1.2 / 20 000 000 / 1.9 / 250 000). _manual_review больше
не принимает settings. Осиротевшие комментарии Settings к уже снятым в #2475
флагам (#1871 P1.2, P2 radius-dedup) удалены.
Тесты: OFF-тест дедупа удалён; два теста, пинившие дедуп OFF ради изоляции,
получили разные площади у аналогов (разные физлоты). Регрессионный гейт и
реплей бэктеста по сделкам (1600, из них 615 радиусных) побитово те же.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tier H («тот же класс дома») отдаётся только при ≥5 комплах, иначе молча уходит в
Tier W без фильтра по классу. Прод, Loki 30 сут (комментарий 12.09): 61 из 86
попыток не доживает, в 31 из них в своей полосе 0 комплов. Какой вариант лучше —
порог 5→4, окно этажей ±30%→±50% или оба — решает только MAPE бэктеста на сделках,
а оба числа были литералами: сравнить варианты без правки кода было нельзя.
estimate_tier_h_min_comps (5) и estimate_tier_h_floors_tol (0.30) в Settings,
_fetch_analogs берёт их оттуда. Дефолты дают те же tf_min/tf_max, что литералы
(сверено для total_floors 1..199), поведение прода не меняется. Бэктест варианта:
docker exec -e ESTIMATE_TIER_H_MIN_COMPS=4 -e ESTIMATE_TIER_H_FLOORS_TOL=0.5 …
Дефолт не меняю: бэктест вариантов не прогнан (нужен деплой этих ручек).
Refs #3234
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1971, два невыполненных пункта DoD.
1. Уведомления не было. Заявка с результата оценки только ложилась в
trade_in_leads (docstring прямо называл уведомление «вне scope»). На проде
17.09: 4 заявки, notified_at пуст у всех, последняя 12.07. Теперь после ответа
клиенту фоновая задача шлёт сообщение в support-топик тем же ботом, что и
веб-чат поддержки, и при успехе ставит notified_at. Отказ Telegram не меняет ни
ответ (200), ни сохранённый лид — только лог. Телефона в сообщении нет: копию в
Telegram не стирает механизм удаления ПДн, поэтому туда идут id заявки,
пользователь и id оценки.
2. Доли заявок от оценок не было нигде. Панель на продуктовом дашборде: лиды за
7 суток / успешные оценки за 7 суток (знаменатель — только outcome=ok: форма
заявки показывается только при посчитанной оценке). Выражение проверено на
боевом Prometheus 17.09: 0 / 91.008 = 0.
Closes#1971
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раунды игры — строки витрины, а витрина с 12.09 отобрана по ошибке МЕРЫ
(полоса -5..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» выходила без
оговорки и читалась как точность расчёта: на проде 17.09 медиана |ошибки|
двадцати строк 6,5 %. Лента и таблица «Точность» полосу называли, игра — нет.
Теперь итог печатает медиану по всей сверке (landing-facts) и, если все
строки витрины лежат в полосе (allWithinBand, как у ленты и таблицы),
говорит, что квартиры из отобранной полосы, а не из всей сверки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Регексы названия улицы в _parse_street_house пускали ведущую цифру только через
пробел («8 Марта»), а через дефис нет. «40-летия Комсомола», «22-го Партсъезда»,
«3-го Интернационала» разбирались так:
- avito «ул. 40-летия Комсомола,32Г» и yandex — ('', ''), дедупа нет вовсе;
- cian «мкр. ЖБИ, улица 40-летия Комсомола, 8А» — ('mkr жби', '40'): все дома
улицы получали один ключ и дом «40», разные квартиры с одинаковыми
этажом/площадью/ценой сливались в одну;
- domklik «60-летия Октября проспект» — ('октября', …).
Фикс: (?:\d+\s+)? → (?:\d+(?:\s+|-))? в _DEDUP_STREET_NAME_RE и
_DEDUP_STREET_NAME_SUFFIX_RE. На 5310 живых адресах прода с «цифра-буква»
меняется разбор у 730 объявлений, все новые токены — реальные нумерованные
улицы (14 штук, от «40-летия комсомола» до «2-ая новосибирская»).
Бэктест-гейт: baseline перегенерирован. В реплее дедуп включён (как на захвате),
поэтому правка разбора сдвигает пул: изменились 99 из 1600 сделок, у 48 ошибка
меньше, у 51 больше. expected_sold MAPE 15.88→15.92, покрытие вилки 1056→1057,
headline не сдвинулся.
Refs #2291
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обработчик landing_showcase_deals безусловно ставил прогону done. У счётчиков
витрины (considered/eligible/written) нет результатного ключа кита, поэтому
сводка просроченных судит её только по статусу: прогон с written=0 обнулял
часы свежести так же, как удачный, а страница тем временем теряла таблицу.
Теперь written=0 — mark_failed с причиной и logger.error. Тест идёт путём
сводки: обработчик -> freshness_rows -> stale_sources; пустой последний
прогон при старом непустом даёт витрину в тревоге, непустой — нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
После отправки оценки freshResult оставался выставленным, и клик по строке
истории ничего не менял на экране: useEstimate получал null (восстановление
по id вообще не запускалось), а estimate и currentEstimateId брались из
только что посчитанного результата. onSelectEstimate теперь сбрасывает
freshResult так же, как это делает handleNew.
Тест рендерит страницу с заглушками хуков: submit, затем клик по другой
строке истории — в useEstimate уходит id строки, HeroBar и форма получают
её id и адрес. Без правки тест красный (useEstimate вызван с null).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ДомКлик: DomClickGeoProfile вместо зашитых _EKB_ADDRESS_GUID/_EKB_AREA_ID; GUID Москвы
и области проверены живьём, aids вне ЕКБ не нужен, гард по bbox профиля. Страница
прогрева 77/50 — апексный domclick.ru: субдомены msk./moskovskaya-oblast. отдают 301.
Циан/Авито/Яндекс: CityLocation.cian_host, три новых скоупа (moskva, moskovskaya_oblast,
moskva_i_mo), avito_slug_is_region, city=NULL у мультигородских скоупов. Неизвестный слаг
теперь падает с ValueError вместо молчаливого отката на Екатеринбург.
Якоря: Москва — сетка 25 точек под radius_m=8000; область — 22 города-спутника
(10 добраны из Nominatim) плюс 22 кластера лот-массы. Замер на проде: города радиусом
10 км дают 73.8% лот-массы области, вместе с кластерами — 95.2%.
Ценовые коридоры: планировщик plan_price_corridors со статистикой усечения плюс
BisectionStats в живом движке. Провайдеры их пока не передают — отдельный заход.
Расписаний scrape_schedules для 77/50 в этом PR нет: они пойдут после первого ручного
прогона, подтверждающего живость профилей.
Центроид строился по названию улицы без города — в области это схлопывало одноимённые
улицы разных городов. Ключ стал (region_code, населённый пункт, улица); НП берётся из
типа сегмента адреса, из реестра городов региона или из deals.city; при пустом НП ключ
у области отбрасывается, а не склеивается с чужим городом.
Фильтры по региону добавлены в SELECT кандидатов, в запрос домов и в UPDATE. Новый
--max-spread-km (5 км) отбрасывает бакет с разбросанными домами.
geocoder.py: поддержка региона 50 (маркер «московская» — намеренно не «москва»,
DaData-имя «Московская», новое поле Region.has_city_core=False, чтобы к адресу области
не приклеивался суффикс главного города).
Обе стороны раскладываются по одной сетке 0.1°x0.2°, медианы внутри ячейки, в итог
только ячейки с ≥10 сделок И ≥10 объявлений, агрегация взвешена числом сделок.
Замер на проде (SQL исполнен в транзакции с ROLLBACK): регион 50 — 0.8136 вместо
пулового 0.8851, регион 77 — 0.7287 вместо 0.7100. Разнонаправленный сдвиг — подпись
поправки состава. Регион 66 не тронут байт в байт.
Гарды: регион не получает строк при покрытии geom <50%, совпавших ячейках <3 или
попадании в пересечение <50% геокодированных сделок; DELETE идёт в любом случае.
Раздел /articles с 29.08 стоял с одной живой статьёй и двумя карточками без
ссылок. Обе дописаны — но не по темам из макета: те темы данными не
обеспечены.
«Сколько стоит метр в Екатеринбурге: цены сделок, а не объявлений».
24 535 договоров Росреестра за июль 2025 — июнь 2026, регион 66. Медиана
123 626 ₽/м². Несущий факт статьи контринтуитивен и проверен: кривая цены
метра по комнатности не падает, а прогибается — дешевле всего метр в
двухкомнатной (116 910 ₽), дороже всего в студии (134 507 ₽). Плюс
поквартальная динамика за все десять доступных кварталов (+20,6%) и восемь
городов области с порогом публикации в сто сделок.
«Почему квартира не продаётся: что происходит с ценой, пока вы ждёте».
49 620 объявлений области с историей цены площадок. Цена двигалась у 43,5%,
из них вниз у 60,2%, вверх у 38,0%. Медиана снижения −3,85% и −291 000 ₽.
ПОЧЕМУ НЕ ТЕ ТЕМЫ, ЧТО В МАКЕТЕ. Карточка «Сколько продаётся квартира»
требовала срока экспозиции, а он не измеряется: `days_on_market` пуст у всех
435 тысяч сделок Росреестра, а снятие объявления и продажа в наших данных
неразличимы — 41% «снятий» приходится на шесть дат, совпадающих с провалами
собственного обхода. Мерили бы мы свой сборщик, а не рынок. Вторая карточка
называлась «Завысить и торговаться»; запрос «завысить цену на квартиру»
уведён юридическим смыслом (завышение суммы в ДКП ради ипотеки), и статья
попала бы в чужую выдачу.
ДИСЦИПЛИНА ЧИСЕЛ. Замер прошёл адверсариальную проверку: из десяти
первоначальных величин воспроизвелись две, остальные отброшены как
непригодные к печати. Обе статьи пересчитаны заново с нуля. У каждого блока
стоит своё поле source/footnote с датой, выборкой и границей — чем это число
НЕ является. Неизмеримое названо неизмеримым прямо в тексте, а не обойдено
молчанием: первый раздел второй статьи объясняет читателю, почему средний
срок продажи не назовёт никто честный.
Обвязка: два ключа в PUBLIC_ROUTES, две страницы, sitemap, три матчера в
apps.caddy на каждый адрес, PUBLIC_SHORT_PATHS у RouteGuard,
PUBLIC_INDEXED_FILES в гейте noindex. Блок «Читайте дальше» перестал быть
статичным «СКОРО» и делает карточку ссылкой, как только у неё появился
route. Витрина и лэндинг обновились сами — обе перебирают ARTICLE_CARDS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ8jqr4SFirX5tFLwdSrXh