Общая тема: скрапер тихо возвращал пустоту вместо сигнала об ошибке, продукт
считал оценку по остаткам данных, не зная что источник отвалился.
1. avito serp.py (medium): citywide/byrooms/exhaustive sweep больше не путает
DOM-drift с "объявлений нет". _is_unexpected_empty_page() перепроверяет
независимый сигнал (_extract_total_count — счётчик page-title/count либо
no-results маркер): page=1 c 0 карточками и БЕЗ no-results маркера →
AvitoContentBlockedError (переиспользует существующий механизм #754/#779).
Exhaustive-бакеты (_paginate_leaf_bucket) получают expected_total из probe —
total>0 но 0 карточек после пагинации всех страниц тоже поднимает ошибку.
page>1 c 0 карточками остаётся graceful end-of-pagination (не regressed).
2. cian serp.py (low): _parse_serp_html сравнивает totalOffers vs
results.offers из ОДНОГО state-блоба. totalOffers>0 при пустом offers —
report как schema regression (elevated до ERROR + Glitchtip через новый
_report_schema_regression(), общий с существующей offer-level охраной).
Порог 0-vs->0 выбран т.к. это единственный позиционно-независимый сигнал:
функция не знает номер страницы, дробный порог ложно сработал бы на
легитимной последней частичной странице exhaustive-пагинации.
3. avito detail.py (low): publish_date больше не завышает свежесть лота на
границе года. Если "текущий год + месяц/день" даёт дату в будущем
относительно момента парсинга (декабрьское объявление читается в январе) —
откатываем на год назад.
4. cian_session.py (low): verify_session больше не конфлирует expired/
source-down/markup-changed. Новые VERIFY_SOURCE_UNAVAILABLE_SENTINEL (5xx,
network/timeout exception) и VERIFY_MARKUP_CHANGED_SENTINEL (HTTP 200 но
header-frontend auth-state не распарсился) отделены от None (единственный
сигнал реального логаута: 401 или isAuthenticated=false). Callers
(product_handlers.py::_cian_pre_claim, admin.py) НЕ обновлены — вне
разрешённого scope этого PR (только cian_session.py + scraper-kit),
отдельный follow-up.
Тесты: 19 новых (8 avito sweep drift, 6 cian totalOffers mismatch, 5 date
rollover) + обновлены 2 существующих (cian_session markup-changed вместо
None) под новый, более точный контракт. Полный backend suite:
2649 passed, 8 skipped, 1 deselected (без регрессий).