Четыре live-теста из правок по ревью без БД пропускаются, а гейт conftest
роняет прогон с DSN-заглушкой на необъявленных пропусках (rc=1 при
«6226 passed» в сводке). Внесены в skip_allowlist.txt к остальным тестам #3299.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью PR #3565: у нового предиката резерва две части не проверялись ни одним
тестом. Снятие `other.expires_at` из подзапроса acquire и снятие
`other.consecutive_fails` из подзапроса mark_banned оставляли прогон
test_3299 + services/test_proxy_pool.py зелёным (72 passed). Проверено тем же
способом, и нашлась третья такая часть: `other.expires_at` в mark_banned.
Добавлено по live-тесту на каждое условие: просроченный резерв в acquire,
резерв в карантине и просроченный резерв в mark_banned.
Попутно найден дефект рядом. Внешний отбор mark_banned («есть ли у источника
другой узел») срок аренды не проверял, хотя acquire такой узел не выдаёт, а
докстринг обещает «тем же правилом, что acquire()». Если у источника остались
только просроченные узлы, бан уходил последнему живому, и источник оставался
без прокси, пока healthcheck не наберёт просроченным узлам отказов. Добавлена
проверка `sp.expires_at` и тест на этот случай.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью PR #3561: три мутации гейта оставались зелёными — порог `attempted >= 2`,
жадный гейт `failed >= 1` и снятый `houses_attempted += len(nb_id_list)` в ветке
«houses DB query failed». Добавлены проверки по чекпоинту:
- 1 из 2 домов отказал в каждом якоре — оба якоря в чекпоинте (контроль жадности);
- единственный дом якоря отказал — якорь не в чекпоинте (порог);
- упал запрос домов — якоря не в чекпоинте, houses_attempted == houses_failed == 2.
Код не менялся.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью PR #3561, две находки.
1. Ручные запуски из админки (avito/cian city sweep, cian_full_load,
yandex_full_load, yandex_city_sweep) идут мимо scheduler._dispatch: в except
был только logger.exception, и отказ «пул пуст» до try-финализатора пайплайна
оставлял строку running до zombie. Логика финализации переехала из
планировщика в runs.mark_crashed; её зовут планировщик и все пять ручек.
2. Обычный путь пайплайна — mark_failed и raise; планировщик звал mark_failed
второй раз. UPDATE — no-op, но _alert_on_run_id срабатывал снова с тем же
стриком, и на вехе лестницы в Sentry уходил дубль (+ WARNING «no-op»).
mark_crashed сначала читает статус и финализирует только running.
Тесты по значению над двойником строки scrape_runs и боевыми mark_*:
статус banned/infra или failed после планировщика и после каждой из пяти ручек,
одна отправка в Sentry на неудаче пайплайна.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3556: settings_at_capture в _write_fixture писал только поля estimate_*
из Settings. После переноса 31 порога в константы estimator.py будущая фикстура
не знала бы, с какими порогами её захватили. Теперь в снимок попадают и
числовые константы модуля (63 штуки, CORRIDOR_CLAMP_MIN_N в том числе).
replay_fixture снимок по-прежнему не читает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3556: подмена rooms_boost=1.0 на месте вызова _compute_same_building_anchor
проходила весь сьют — тест значений держит только число в константе. Новый тест
оборачивает расчёт якоря и требует боевые sigma/rooms_boost/floor_sigma/min_comps/
mad_k (прод 17.09.2026). SB_GUARDRAIL_TOL, FSD_K, SB_GATE_MAX_FSD и сдвиг
ANCHOR_TIER_C_CORRIDOR_MULT вниз по-прежнему без поведенческих тестов.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3556: тест значений держит число в константе, но не то, что код её читает.
Подмена min_n_deals=5 на обоих местах вызова проходила весь сьют. Хуже: Guard-2 и
bimodal-гард снимались целиком (`if False:`) при зелёном test_estimator_quarter_index —
оба теста были пустыми. Guard-2: заглушка отдавала аналогам индекс цели, factor=1.0.
Bimodal: аналоги без кадастра, поправку гасил Guard-5 (#2583) независимо от гарда.
- оба теста получили аналоги с известным уровнем 1.0 — без гарда цена сдвигается;
- границы: доля ровно 0.6 поправку даёт, индекс ровно 2.0 даёт, 2.01 — нет;
- заглушки lookup'ов пишут min_n_deals, тест требует 10 на обоих вызовах.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три правки в комментариях tradein-mvp/frontend (имена estimate_corridor_clamp_min_n
и estimate_imv_blend_weight → константы) кода не меняли, но фильтр deploy-tradein
по tradein-mvp/frontend/** пересобирал и пересоздавал tradein-frontend ради
комментариев. Комментарии переименуются вместе с ближайшей правкой фронта
(в #3554 фронт и так пересобирается — переименование приложено к нему патчем).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью PR #3561: тест таймаута фазы закреплял как контракт done_buckets == ['st'].
Лоты ДомКлика копятся в памяти и пишутся одним save_listings после всех корзин,
поэтому снятая watchdog'ом (или упавшая на save) фаза не сохраняет ничего, а
чекпоинт всё равно получал completed_buckets живого скрейпера — следующий прогон
пропускал корзину навсегда (механизм миграции 308).
Чекпоинт пополняется только если фаза дошла до конца (флаг _saved после save).
Тест развёрнут: при таймауте fetch_city и при падении save_listings
done_buckets == [], статус failed. Контроль «полный проход несёт
унаследованное ∪ пройденное» — test_3369.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3552: направление расходится с #3556/#1970, где неменявшиеся
estimate_*-настройки переводят в константы движка. Две новые ручки нужны
только для бэктеста вариантов; условие возврата записано рядом с полями,
чтобы его увидел тот, кто будет делать шаг 4 #3234.
Refs #3234
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Что было. После «40-летия Комсомола» остались однобуквенные порядковые:
регексы требовали после «N-» минимум две буквы. Одна улица в трёх форматах
разбиралась по-разному: cian «улица 2-я Синичкина» давал ('', ''),
avito/yandex «2-я ул. Синичкина» и domklik «2-я Синичкина улица» теряли номер
('синичкина'). Потеря номера — это ложная склейка: «9-я Парковая, 5» и
«12-я Парковая, 5» получали один ключ ('парковая', '5').
Что сделано. В обоих регексах названия улицы добавлен необязательный
порядковый «N-x » перед (нумерованным) именем; формат «номер тип имя»
переставляется в «тип номер имя» до разбора. Номер дома с дефисом не тронут.
Замер на проде (tradein-postgres, SELECT, 17.09): 12 848 разных адресов с
шаблоном «цифра-дефис/пробел-буква», 29 959 объявлений. Разбор изменился у
8 713 объявлений; ни один адрес не потерял токен. Межисточниковых групп
(улица, дом) 1305 -> 1473. Из 81 прежней группы, которая распалась, 60 —
разные порядковые улицы, 3 — ложный mkr-ключ, 18 — голая и порядковая
улица в разных городах (одна — источник сам потерял номер). Гейт бэктеста
без изменений baseline.
Refs #2291
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Два новых теста класса 4 ставили monkeypatch.setattr(settings,
"estimate_dedup_analogs_enabled", True). PR #3556 (#2378) удаляет это поле из
Settings, и setattr по несуществующему атрибуту падает AttributeError. CI
гоняет голову ветки, а не merge-ref, поэтому второй смерженный PR пришёл бы
зелёным и уронил main. На main флаг и так ON по умолчанию
(test_dedup_default_is_on), так что строки лишние при любом порядке мержа.
Проверено: дифф PR, наложенный на голову #3556 (31c31f8f), до правки
2 failed / 144 passed (AttributeError), после 146 passed. Тесты не стали
пустыми: откат префиксного регекса без флага даёт assert 4 == 1 и 1 == 2.
Refs #2291
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3554: вызов corridorAdvisoryNote в HeroSummary (корневая /trade-in)
тестом не покрывался — мутация F2 убирала аргумент estimate.analog_tier,
vitest оставался зелёным (305 passed). Покрыт был только v2 mappers.
Рендер HeroSummary по образцу thinMarketBadge.test.tsx: n=20,
advisory_only=false; same_building → подпись про дом в тексте hero,
micro_radius → подписи нет. Мутант F2 краснеет: «expected 'Секция 1 ·
Сводка…' to contain 'справочно: цена посчитана по аналогам…'».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ревью #3554: замена `>=` на `>` в гейте radius-floor оставляла
test_estimator_radius_floor.py зелёным — в тестах были только count=15 и
count=min_n−1. При n == min_n витрина не пишет «справочно»
(advisory_only=False), значит floor обязан поднять цену.
Новый тест: n = estimate_corridor_clamp_min_n, медиана 80k → ровно 120k,
плюс проверка, что DkpCorridor на той же границе не справочный. Мутант
`>` краснеет: «коридор из 10 сделок (ровно порог) должен поднять цену
ровно до floor=120000».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Сверка парсера с живым __SSR_STATE__ трёх карточек (17.09) нашла три тихие дыры:
1. owners_count. egrnData отдаёт поля ЕГРН обёрткой {status, value}; int() от
словаря молча давал None. Прод: 0 из ~2970 карточек, обогащённых с 29.08,
против 5075 из 6296 у ручного прогона 18.07, который обёртку разворачивал.
Ключ egrnData.area подтверждён — гадания rosreestrArea/object_area удалены,
в raw_payload обёртка остаётся как есть (status = сверка с объявлением).
2. livingArea/kitchenArea = 0 у Домклика значит «не указано», а в колонку ложился
0.00 и затирал известное значение. Прод: 303 кухни и 84 жилые площади = 0,
только domklik. Разбор: _pos_float; строки: миграция 320 ставит NULL.
3. Жилую площадь и балконы стирал переобход выдачи: апсерт scraper_kit.base писал
их сырым EXCLUDED, а SERP Домклика и Авито этих полей не отдаёт. Прод: domklik,
переобойдённые после обогащения, living_area_m2 0 из 5217 (без переобхода
2990 из 4058); avito 1 из 8639 (4122 из 6505). COALESCE в SET, в гейте #2992
и на reconcile-пути.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Без БД 11 новых live-тестов пропускаются, и гейт conftest роняет прогон
(«НЕУЧТЁННЫЙ ПРОПУСК», rc=1 при «6226 passed» в сводке). В ci-tradein.yml они
идут по-настоящему: локальный прогон на Postgres со схемой из миграций —
6278 passed, 1 skipped, rc=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fetch_detail без session/browser_fetcher входил в синхронный curl_proxy_url:
acquire/mark_health/release RealProxyProvider'а ходят в БД прямо на loop'е.
Вызывающий — админ-ручка истории цен Циана в публичном tradein-backend
(один воркер uvicorn): пара блокирующих вызовов на каждый листинг батча.
#3398 перевёл так три сайта /estimate, этот остался.
Теперь async with acurl_proxy_url: вход и выход в потоке, contextvars
(current_run_id) копируются, ProxyBanError/CianBlockedError изнутри блока
доходят до пула как раньше (test_2700/test_3402 зелёные). Попутно потолок в
докстринге acurl_proxy_url: pool_timeout теперь 5 с (#3444), не 30.
Тест по образцу test_3398: acquire спит 0.3 с, соседняя корутина тикает.
На синхронном входе — «тиков всего 0».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Все 7 скачков ×10 в yandex-истории цен на проде (17.09) — не «сдвиг на следующем
наблюдении», как предполагал issue, а синтетический посев из gate-поля
price.previous: нижняя точка стоит ровно на 24 ч раньше второй и записана тем же
батчем. Ошибочна нижняя цена (13–37 тыс/м² против 133–375 у верхней). Ещё 4 посева
того же рода — ×175…×1000 (цена в тысячах). Ветка писателя «история есть» на проде
не записала ни одной строки: смену цены раньше неё пишет триггер
record_listing_price_change, поэтому отлов «на следующем наблюдении» там поймал бы 0.
- yandex_price_history: price.previous, отличающаяся от текущей цены в 9.5 раза и
больше в любую сторону (или не > 0), не сеется; счётчик seed_rejected в логе.
- миграция 310: удаляет yandex-посевы по подписи батча (recorded_at совпадает,
change_time второй точки ровно +24 ч) с тем же порогом; lock_timeout 5s,
остановка при > 30 строк; на проде ровно 11 кандидатов, повторный прогон — 0.
- тесты по значению: посев отвергнут/сохранён на прод-ценах; настоящий файл
миграции на живом Postgres через временную таблицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #3405 писал scrape_runs.proxy_id только из proxy_pool.acquire(). Прогоны,
которые берут прокси через proxy_egress.resolve_proxy_url без аренды
(yandex_detail_backfill, yandex_address_backfill, curl-ветка
avito_detail_backfill), атрибуцию не получали: прод 17.09 —
yandex_detail_backfill 0 из 56, yandex_address_backfill 0 из 1.
resolve_proxy_url при выбранном узле и выставленном current_run_id зовёт
attribute_run_proxy. Своей короткой сессией: db вызывающего — долгоживущая
сессия прогона посреди работы, а атрибуция коммитит и на сбое откатывает.
Тесты на живом Postgres: атрибуция внутри прогона (proxy_id и
counters.proxy_ids), вне прогона scrape_runs не тронут, незакоммиченная
работа вызывающего не коммитится. Красные прогоны: на старом коде
«assert None == 70»; с атрибуцией через db вызывающего — «атрибуция
закоммитила чужую транзакцию».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Защита последнего узла в mark_banned бережёт узел от бан-строки и пишет
«узел продолжит выдаваться», но curl_proxy_url на том же ProxyBanError
вслед за баном звал mark_health(ok=False). Три ответа 403/капчи подряд —
consecutive_fails = 3, и acquire отсекал узел для ВСЕХ источников: пул
пуст при живом узле. Браузерный путь это исправил в #3288
(report_platform_ban), curl-путь — нет; через него ходят cian detail,
ЖК-резолв, история цен Циана и оценщик.
Теперь на ProxyBanError зовётся mark_banned вместо mark_health(False).
Транспортные сбои по-прежнему засчитываются узлу.
Тест на живом Postgres настоящим трактом (curl_proxy_url →
RealProxyProvider → proxy_pool): единственный узел, три CianBlockedError →
consecutive_fails == 0, acquire('cian') выдаёт узел; на старом коде
«assert 3 == 0». Пять старых проверок ждали mark_health(False) на бане —
они фиксировали дефект, поправлены.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Порог BROWSER_NAV_TIMEOUT_MS=60000 не с чем было сравнить: «fetch OK» писался
на DEBUG и без длительности, «fetch error» — тоже без неё, а время из
access-лога включает пейсинг (18 с у cian) и ожидание лока. За сутки до 17.09
у cian 54 TimeoutError на ~1350 страниц (90 recycle × 15), ~4 %.
Теперь _fetch_once меряет только целевую page.goto (monotonic, в finally —
и на таймауте), обе строки несут nav_ms, «fetch OK» переведён на INFO.
nav_ms=None — до цели не дошли (упал прогрев origin), прошлое число не
наследуется. Решение по порогу — после 48 ч данных, это отдельный шаг.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Запасной заход acquire() и защита последнего узла в mark_banned() спрашивали,
есть ли у выделенной привязки ВТОРОЙ узел той же привязки. Выделенный узел
штучный, поэтому ответ почти всегда «нет», и такой узел не выдавался никому,
хотя свой источник обслуживали 'any'-узлы. Прод 17.09: узел 15 (avito)
свободен и здоров, а cian/domclick его не получали; 30.08 так лёг добор
Домклика (прогоны 5449-5459).
Теперь резерв — любой узел той же привязки или 'any', здоровый
(consecutive_fails), с живой арендой порта и не забаненный источником
выделенной привязки. Предикат одинаковый в обоих местах. leased_by
намеренно не проверяется: аренда временная.
Тесты на живом Postgres (tests/test_3299_*): 6 случаев из приёмки, два
красные на старом SQL. Мок test_proxy_pool.py: маршрутизация primary/fallback
по полному фрагменту IN (:provider, 'any') и гейт нового предиката.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/login сайдкара не принимал proxy из тела (_no_live_proxy(provider, None),
_ensure_browser без override), BrowserFetcher.login его не клал, а ручка
cian auto-login сознательно не брала провайдер. Итог: логин всегда шёл с
SCRAPER_PROXY_URL сайдкара — на проде это выключенный узел 9 (InvalidIP),
то есть ручка восстановления cian-сессии не работала вовсе.
- сайдкар: /login берёт proxy из тела, как /fetch (guard и _ensure_browser
с override; crash-relaunch в _do_login сохраняет _launched_proxy);
- scraper-kit: login() кладёт в тело узел текущей аренды, как _post_fetch;
- backend: ручка передаёт _kit_proxy_provider(), как debug-карточка DomClick;
пустой пул на проде — 503 «нет свободного узла для cian», а не 502.
Тесты #3197 про «логин без пула» переписаны на обратные утверждения.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
avito/imv.py сам собирал curl_cffi AsyncSession с третьей копией
document-заголовков (_DOC_HEADERS) — из-за того, что parity-тесты патчили
curl_cffi.requests.AsyncSession, а _base импортирует класс на уровне модуля и
такой патч до него не долетает. Взят вариант B из issue: в кодовой базе уже
принят патч scraper_kit.providers._base.AsyncSession (test_scraper_proxy.py,
test_pipeline_browser_routing.py, test_kit_serp_proxy_pool.py), а вариант A
(живой lookup в _base) сломал бы эти тесты — атрибута _base.AsyncSession не стало бы.
Сессия теперь build_document_session(proxy_url, timeout=25); _DOC_HEADERS
удалён (идентичен DOCUMENT_HEADERS, проверено до правки), мёртвый
try/except ImportError убран — curl_cffi и так импортируется через _base.
Тесты патчат _base.AsyncSession; ассерт с config дополнительно фиксирует
impersonate, timeout=25 и заголовки — параметры сессии не изменились.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
После удаления 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>
Канон-ключ tradein_canon_addr не срезал район, приклеенный к номеру дома без
запятой (наследие слипа «29р-н» из #1773): «ул. Евгения Савкова, 17Ар-н
Академический» давал «евгениясавкова17арнакадемический», а «44Бр-н» терял
литеру (токен-срез принимал «бр» за бульвар). Двойник «ул. Евгения Савкова,17А»
с таким домом в один кластер не попадал, и перепись остатка его не видела.
Миграция 311: тело 147 плюс шаг «<цифра>[литера]р-н <название>» → «<цифра>[литера]»
сразу после lower. Якорь на цифре обязателен: без него «мкр-н Кутузовский, 2»
превращается в «мк2». Прод 17.09 (read-only): канон меняется у 44 из 49 159
домов, у 20 появляется двойник в пределах 250 м, ГАР-сторона не затронута
(0 из 3,27 млн строк с шаблоном).
Тест по значению на функции, установленной миграциями, и сквозное слияние:
склеенный адрес и чистый двойник сливаются, «17» без литеры остаётся отдельным.
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>
ФИАС-проход house_dedup_merge шёл без гео-стража (#2187: «общий ФИАС-UUID и
есть здание»). Посылка неверна: house_fias_id — ответ DaData на НАШУ строку
адреса, и «улица Кирова, 4» без города получает тот же UUID, что и дом в
другом городе. Прогон 05.09 (house_merge_log, merge_pass='fias'): 408 слияний,
129 дальше 250 м, 53 дальше 3 км, максимум 362 км, у 67 нет координат на одной
из сторон.
Теперь у обоих проходов один и тот же страж (250 м, координаты у обеих сторон),
параметр apply_geo_guard удалён — асимметрию нельзя вернуть флагом.
Тест по значению на живой Postgres: тот же ФИАС в 5 км и с пустыми координатами
не сливается, тот же ФИАС в 10 м сливается, журнал пишет geo_guard=true.
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>