Продолжение #2659 после PR-A. У джобы деактивации не было НИ ОДНОГО
ограничителя ОБЪЁМА: min_confirmations может пропустить прогон целиком,
пол переобхода поднимает TTL, cap_mult ограничивает сам пол — но ни один
не смотрит на РАЗМЕР ВЫБОРКИ, из которой посчитан квантиль, и ни один не
смотрит на то, сколько строк UPDATE снимет. UPDATE идёт одним statement'ом
без LIMIT: при обвале любого гейта выше снимается сколько снимется.
Добавлено два рельса, оба внутри revisit_floor_quantile > 0:
1. Гейт деградации пола. floor_n_pairs (счётчик из PR-A) ниже
min_floor_pairs=30 ЛИБО упал более чем впятеро относительно предыдущего
успешного прогона ТОГО ЖЕ РАСПИСАНИЯ -> skipped_floor_degraded, ни одна
строка не тронута. Сравнение идёт «прогон сам с собой» по
scrape_runs.source, а не по listing_source: у cian и cian_null_segment
listing_source один, но это две серии с несопоставимым масштабом
выборки — сравнивать их было бы категориальной ошибкой.
2. Аварийный потолок max_deactivated=15000. Preflight count(*) по ТОМУ ЖЕ
предикату, что исполнит UPDATE — abort ДО записи. Не рабочий порог, а
предохранитель: исторический максимум легитимного снятия 9 300 (avito
06.06), запас 1.6x.
Порога по ДОЛЕ пула сознательно НЕТ: пул, с которым джоба реально работает,
никогда не записывался, а восстановленный постфактум ряд даёт 40 %, 97 %,
317 %, 1652 % — калибровать не на чем. Поэтому deactivation_candidates,
active_pool и deactivated_pct пока только ПИШУТСЯ.
Валидация параметров — тот же класс, что у ttl_days/cap_mult: bool
отбивается ДО числового сравнения (jsonb-опечатка true/false в
default_params расписания — единственный запланированный способ их задать).
uv.lock: две строки requires-dist подтянуты к pyproject (curl-cffi
>=0.7.0 -> >=0.15.0). Это устранение устаревшей записи, а не смена
зависимости — разрешённые версии не изменились; лок расходился с
pyproject на main и регенерировался при любом вызове uv.
Проверено: 249 passed, 1 skipped (-k "deactivate or stale").
Refs #2659