"""`app.services.scrape_runs` — АЛИАС `scraper_kit.orchestration.runs` (#3390). Реализация одна, и она в kit. Здесь нет ни функций, ни констант: модуль подменяет себя kit-модулем в `sys.modules`, поэтому `from app.services import scrape_runs as runs_mod` и `from scraper_kit.orchestration import runs` дают ОДИН И ТОТ ЖЕ объект. Зачем так, а не «тонкий модуль с реэкспортом имён»: реэкспорт разводит патч-цели. `patch("app.services.scrape_runs.sentry_sdk")` или `patch.object(runs_mod, "mark_done")` правит глобаль ЭТОГО модуля, а тело реэкспортированной функции читает глобаль СВОЕГО — kit'а. Тест остался бы зелёным, не подменив ничего (или покраснел бы на пустом месте), а прод-путь оказался бы вне проверки: ровно тот класс дефекта, из-за которого заведена #3390. С алиасом патч по любому из двух имён попадает в единственную реализацию. Что здесь было до #3390: полная вторая копия модуля (1159 строк), которая разошлась с kit'ом минимум четырежды — counters ЗАМЕНЯЛА (`counters = CAST(:counters AS jsonb)`) против мержа (`COALESCE(counters,'{}') || …`) у kit, свой гейт по статусу, `honors_cancel` только здесь, `mark_skipped` только там. Ревью дважды за сутки делало из этого ложные выводы (#3388, #3355), потому что «вызывающий пишет через runs» не отвечало на вопрос, какая из двух семантик применится. Победила семантика kit'а — мерж: у одной строки прогона несколько писателей (пульс, финализатор, дрейн), каждый знает лишь свои ключи, и замена теряла чужие (чекпоинт `done_buckets` #930, метка `interrupted` #3391, замер из пульса #3384). Зависимости «финализатор обязан УДАЛИТЬ ключ заменой» нет ни у одного вызывающего: строка прогона создаётся пустой (`create_run`), а резюм читает counters ПРЕДЫДУЩЕГО прогона по его id. НЕ добавляй сюда код: всё, что написано ниже подмены, недостижимо — импортирующий получает kit-модуль. Новые функции — в `scraper_kit/orchestration/runs.py`. """ from __future__ import annotations import sys from scraper_kit.orchestration import runs as _kit_runs sys.modules[__name__] = _kit_runs