fix(tradein/sber): сторож мерит отставание загрузки, а не календарь (#2846) #2849

Merged
bot-backend merged 1 commit from fix/sber-freshness-guard into main 2026-08-12 19:36:21 +00:00
Collaborator

Что было не так

Порог sber_index_max_age_days = 35 недостижим по построению. period_month — метка ПЕРВОГО числа месяца, значит возрасту ≥30 уже на закрытии месяца; плюс лаг публикации источника. За 31 сутки прямых наблюдений монитора (07-13…08-12, scrape_runs.counters) возраст лежал в 46..76 и ни разу не опускался ниже 46. Сторож был истинным 100% времени и нёс ноль бит: при живом источнике и при мёртвом загрузчике писал одно и то же.

Второй порог (монитор, 60 = 35 + запас 25) лежит ВНУТРИ рабочего диапазона. Обещание миграции 212 («такт 7 ⇒ потолок возраста ≈53 < 60») прод опроверг: 2026-08-12 возраст 72 при ПОЛНОМ прогоне загрузки 08-06 (errors=0, upserted=639). Двенадцатые сутки подряд (08-01…08-12) монитор писал ERROR при исправной загрузке. Потолок 53 держался бы, только если бы источник публиковал строго помесячно — он не публикует (июль не вышел и на 42-е сутки).

Форма нового сигнала

Загрузчик тянет ВСЮ серию (limit=1000&offset=0, отсечки по периоду нет), поэтому после прогона с errors=0 AND upserted>0 наш max(period_month) равен максимуму источника по построению. Значит:

последний полный прогон вывод поведение
свежий, max не сдвинулся источник не публиковал молчание правильное
старше двух тактов мы не забрали тревога про загрузчик

status='done' за успех не считается — прогон id=37 имеет done при {errors: 9, upserted: 0}. Живая проверка SQL сторожа на проде: 8 полных прогонов из 9, id=37 отфильтрован, последний 2026-08-06, pull_lag_days=6 при пороге 14 → алерта нет (загрузка исправна, источник молчит).

Порог обоснован замером, а не круглым числом: 2 × scrape_schedules.default_params.interval_days для sber_index_pull, читается из ТОЙ ЖЕ строки, по которой планировщик считает next_run_at. Разъехаться с тактом не может. Все 8 полных прогонов укладывались в свой тогдашний такт (разрывы 14.8 / 20 / 28 суток — 20 объясняется самой миграцией 212, подтянувшей next_run_at на 08-06); двойной запас поглощает один пропущенный цикл, два подряд дают тревогу.

Вопрос задаётся ПО ТАБЛО. Монитор идёт тем же порядком, что оценщик (SBER_COEFF_DASHBOARDS), и берёт ту же серию: max() по таблице маскирует отставшее табло (real_estate_deals 2026-06 против dinamika-tsen-obyavlenii 2026-05).

Два порога сведены в один факт — удалением

settings.sber_index_max_age_days удалён, per-estimate warning в estimator._load_sber_index_series убран. Свежесть считает ровно одно место, второму порогу неоткуда взяться (тест test_no_second_calendar_threshold_in_settings это держит). Мёртвая ручка default_params.lag_allowance_days=25 игнорируется — тест доказывает, что она не может вернуть календарь. Отдельной миграцией её может убрать database-expert; кода это не касается.

fetched_at

Убран из DO UPDATE SET (вариант «завести first_seen_at» отклонён: требует миграции + бэкфилла, который честным быть не может — истинного времени первого появления у 639 лежащих строк нет). Теперь колонка означает «когда мы ВПЕРВЫЕ увидели период», то есть такт публикации источника станет измеримым. Ретроспективу это не возвращает — у существующих строк метка 2026-08-06 и останется. Времени последней загрузки колонка больше не хранит; оно и не нужно — scrape_runs хранит его точнее (с errors/upserted), сторож берёт оттуда. Читателей у колонки в коде нет.

Два вопроса к новому сторожу

  1. Достижимо ли состояние срабатывания? Да, и оно не гипотетическое: тревога = «строк полных прогонов не появляется». В истории таблицы разрывы между полными прогонами достигали 14.8 и 20 суток при нынешнем пороге 14. Отдельная ветка — «полных прогонов не было ВООБЩЕ» (состояние прода 05-31, когда единственный прогон был id=37 с errors=9): тоже алерт.
  2. Промолчит ли он, если загрузчик умрёт совсем? Нет — это ровно то, что он ловит: нет новых полных прогонов → счётчик растёт неограниченно → ERROR каждые сутки, навсегда. Старый сторож на смерть загрузчика тоже «алертил», но неотличимо от нормы (он алертил всегда) — а это то же самое, что молчать. Чего новый НЕ ловит: если источник замолчит навсегда при исправной загрузке. По нашим данным «не публиковал 2 месяца» неотличимо от «публикует раз в 2 месяца», а такт публикации ретроспективно затёрт апсертом. С этим PR он станет измеримым; вернуться к вопросу имеет смысл после 3 наблюдённых публикаций (ориентир — ноябрь 2026).

Красный прогон

Новые тесты на origin/main (отдельный worktree, тот же файл):

FAILED test_prod_replay_healthy_loader_silent_source_no_alert
>       assert out["alert"] == 0
E       assert 1 == 0

FAILED test_alert_tracks_loader_not_calendar
        assert healthy["age_days"] == stalled["age_days"] == 72
>       assert (healthy["alert"], stalled["alert"]) == (0, 1)
E       assert (1, 1) == (0, 1)

На ветке: 26 passed. Полный бэкенд-сьют: 4321 passed, 21 skipped.

Двусторонность. test_alert_tracks_loader_not_calendar гоняет ДВА состояния с ОДИНАКОВЫМ возрастом периода (72) и разным состоянием загрузки → (0, 1). На main оба дают 1: сторож не умеет зеленеть при живом источнике — это и был исходный дефект. Плюс граница порога в обе стороны (14 → тихо, 15 → алерт) и test_sber_old_period_with_healthy_pull_stays_silent (событий GlitchTip ноль) против test_sber_pull_stall_becomes_event (событие есть).

Побочное: ярлык сегмента (п.6 задачи) — НЕ чиню здесь, обосновываю

135 строк residential_real_estate_prices несут segment='Первичный рынок', хотя загрузчик просит REAL_ESTATE_TYPE=2 с комментарием «2=вторичка». Значения говорят, что прав ЯРЛЫК, а не комментарий: обл.66 за 6 месяцев 160 474…163 914 ₽/м² против вторичных сделок 97 973…99 547 (+65%) и asking 93 566…101 600; по России 185 709…187 401 против 123 606…127 165 (+50%). Ряд «вторички» от того же издателя не может быть на 65% выше его же индекса вторичных сделок — это новостройки.

Правка требует ЖИВОГО запроса к источнику (какой код REAL_ESTATE_TYPE даёт вторичку), а он в этой задаче запрещён. Денежного эффекта сегодня ноль: табло не входит в SBER_COEFF_DASHBOARDS (убрано в #R2-H1 именно из-за ярлыка), и оба SQL фильтруют segment ILIKE '%вторичн%' — строки до оценки не доходят. При этом одно из двух утверждений в коде ложно, и цена вопроса — один живой запрос. Отдельным issue, а не здесь: если ярлык верен неверно и это на самом деле вторичка, починка вернула бы в оценку серию, убранную #R2-H1, то есть изменила бы вход оценки — что этому PR запрещено.

Предложение, кода не касающееся

Применение якоря не тронуто (замер: 1-3.5% у ~4% оценок, 0.00 пп MAPE). Если вернётесь к нему — заметьте, что на 2026-08-12 применяются ровно два множителя, потому что в 12-месячном окне сделок всего два месяца.

Test plan

  • uv run pytest tests/test_sber_freshness_monitor.py tests/test_alerts_become_events.py tests/test_estimator_audit_fixes.py
  • После деплоя: SELECT counters FROM scrape_runs WHERE source='sber_freshness_monitor' ORDER BY id DESC LIMIT 1 → ждём alert=0, pull_lag_days≈6..13, max_pull_lag_days=14 (и конец 12-суточной серии ложных ERROR)
  • После следующего прогона sber_index_pull (13.08 05:00 UTC): у НОВЫХ периодов fetched_at отличается от 2026-08-06 — колонка начала мерить такт публикации

Refs #2846

## Что было не так **Порог `sber_index_max_age_days = 35` недостижим по построению.** `period_month` — метка ПЕРВОГО числа месяца, значит возрасту ≥30 уже на закрытии месяца; плюс лаг публикации источника. За 31 сутки прямых наблюдений монитора (07-13…08-12, `scrape_runs.counters`) возраст лежал в **46..76** и ни разу не опускался ниже 46. Сторож был истинным 100% времени и нёс **ноль бит**: при живом источнике и при мёртвом загрузчике писал одно и то же. **Второй порог (монитор, 60 = 35 + запас 25) лежит ВНУТРИ рабочего диапазона.** Обещание миграции 212 («такт 7 ⇒ потолок возраста ≈53 < 60») прод **опроверг**: 2026-08-12 возраст 72 при ПОЛНОМ прогоне загрузки 08-06 (errors=0, upserted=639). Двенадцатые сутки подряд (08-01…08-12) монитор писал ERROR при исправной загрузке. Потолок 53 держался бы, только если бы источник публиковал строго помесячно — он не публикует (июль не вышел и на 42-е сутки). ## Форма нового сигнала Загрузчик тянет ВСЮ серию (`limit=1000&offset=0`, отсечки по периоду нет), поэтому после прогона с `errors=0 AND upserted>0` наш `max(period_month)` **равен максимуму источника по построению**. Значит: | последний полный прогон | вывод | поведение | |---|---|---| | свежий, max не сдвинулся | источник не публиковал | молчание правильное | | старше двух тактов | мы не забрали | тревога про загрузчик | `status='done'` за успех не считается — прогон id=37 имеет `done` при `{errors: 9, upserted: 0}`. Живая проверка SQL сторожа на проде: 8 полных прогонов из 9, id=37 отфильтрован, последний 2026-08-06, `pull_lag_days=6` при пороге 14 → **алерта нет** (загрузка исправна, источник молчит). **Порог обоснован замером, а не круглым числом:** `2 × scrape_schedules.default_params.interval_days` для `sber_index_pull`, читается из ТОЙ ЖЕ строки, по которой планировщик считает `next_run_at`. Разъехаться с тактом не может. Все 8 полных прогонов укладывались в свой тогдашний такт (разрывы 14.8 / 20 / 28 суток — 20 объясняется самой миграцией 212, подтянувшей `next_run_at` на 08-06); двойной запас поглощает один пропущенный цикл, два подряд дают тревогу. **Вопрос задаётся ПО ТАБЛО.** Монитор идёт тем же порядком, что оценщик (`SBER_COEFF_DASHBOARDS`), и берёт ту же серию: `max()` по таблице маскирует отставшее табло (real_estate_deals 2026-06 против dinamika-tsen-obyavlenii 2026-05). ## Два порога сведены в один факт — удалением `settings.sber_index_max_age_days` удалён, per-estimate warning в `estimator._load_sber_index_series` убран. Свежесть считает ровно одно место, второму порогу неоткуда взяться (тест `test_no_second_calendar_threshold_in_settings` это держит). Мёртвая ручка `default_params.lag_allowance_days=25` игнорируется — тест доказывает, что она не может вернуть календарь. Отдельной миграцией её может убрать database-expert; кода это не касается. ## `fetched_at` Убран из `DO UPDATE SET` (вариант «завести `first_seen_at`» отклонён: требует миграции + бэкфилла, который честным быть не может — истинного времени первого появления у 639 лежащих строк нет). Теперь колонка означает «когда мы ВПЕРВЫЕ увидели период», то есть такт публикации источника станет измеримым. **Ретроспективу это не возвращает** — у существующих строк метка 2026-08-06 и останется. Времени последней загрузки колонка больше не хранит; оно и не нужно — `scrape_runs` хранит его точнее (с errors/upserted), сторож берёт оттуда. Читателей у колонки в коде нет. ## Два вопроса к новому сторожу 1. **Достижимо ли состояние срабатывания?** Да, и оно не гипотетическое: тревога = «строк полных прогонов не появляется». В истории таблицы разрывы между полными прогонами достигали 14.8 и 20 суток при нынешнем пороге 14. Отдельная ветка — «полных прогонов не было ВООБЩЕ» (состояние прода 05-31, когда единственный прогон был id=37 с errors=9): тоже алерт. 2. **Промолчит ли он, если загрузчик умрёт совсем?** Нет — это ровно то, что он ловит: нет новых полных прогонов → счётчик растёт неограниченно → ERROR каждые сутки, навсегда. Старый сторож на смерть загрузчика тоже «алертил», но неотличимо от нормы (он алертил всегда) — а это то же самое, что молчать. **Чего новый НЕ ловит:** если источник замолчит навсегда при исправной загрузке. По нашим данным «не публиковал 2 месяца» неотличимо от «публикует раз в 2 месяца», а такт публикации ретроспективно затёрт апсертом. С этим PR он станет измеримым; вернуться к вопросу имеет смысл после 3 наблюдённых публикаций (ориентир — ноябрь 2026). ## Красный прогон Новые тесты на `origin/main` (отдельный worktree, тот же файл): ``` FAILED test_prod_replay_healthy_loader_silent_source_no_alert > assert out["alert"] == 0 E assert 1 == 0 FAILED test_alert_tracks_loader_not_calendar assert healthy["age_days"] == stalled["age_days"] == 72 > assert (healthy["alert"], stalled["alert"]) == (0, 1) E assert (1, 1) == (0, 1) ``` На ветке: `26 passed`. Полный бэкенд-сьют: `4321 passed, 21 skipped`. **Двусторонность.** `test_alert_tracks_loader_not_calendar` гоняет ДВА состояния с ОДИНАКОВЫМ возрастом периода (72) и разным состоянием загрузки → `(0, 1)`. На main оба дают `1`: сторож не умеет зеленеть при живом источнике — это и был исходный дефект. Плюс граница порога в обе стороны (14 → тихо, 15 → алерт) и `test_sber_old_period_with_healthy_pull_stays_silent` (событий GlitchTip ноль) против `test_sber_pull_stall_becomes_event` (событие есть). ## Побочное: ярлык сегмента (п.6 задачи) — НЕ чиню здесь, обосновываю 135 строк `residential_real_estate_prices` несут `segment='Первичный рынок'`, хотя загрузчик просит `REAL_ESTATE_TYPE=2` с комментарием «2=вторичка». **Значения говорят, что прав ЯРЛЫК, а не комментарий:** обл.66 за 6 месяцев 160 474…163 914 ₽/м² против вторичных сделок 97 973…99 547 (**+65%**) и asking 93 566…101 600; по России 185 709…187 401 против 123 606…127 165 (**+50%**). Ряд «вторички» от того же издателя не может быть на 65% выше его же индекса вторичных сделок — это новостройки. Правка требует ЖИВОГО запроса к источнику (какой код `REAL_ESTATE_TYPE` даёт вторичку), а он в этой задаче запрещён. Денежного эффекта сегодня ноль: табло не входит в `SBER_COEFF_DASHBOARDS` (убрано в #R2-H1 именно из-за ярлыка), и оба SQL фильтруют `segment ILIKE '%вторичн%'` — строки до оценки не доходят. При этом одно из двух утверждений в коде ложно, и цена вопроса — один живой запрос. **Отдельным issue**, а не здесь: если ярлык верен неверно и это на самом деле вторичка, починка вернула бы в оценку серию, убранную #R2-H1, то есть изменила бы вход оценки — что этому PR запрещено. ## Предложение, кода не касающееся Применение якоря не тронуто (замер: 1-3.5% у ~4% оценок, **0.00 пп** MAPE). Если вернётесь к нему — заметьте, что на 2026-08-12 применяются ровно два множителя, потому что в 12-месячном окне сделок всего два месяца. ## Test plan - [ ] `uv run pytest tests/test_sber_freshness_monitor.py tests/test_alerts_become_events.py tests/test_estimator_audit_fixes.py` - [ ] После деплоя: `SELECT counters FROM scrape_runs WHERE source='sber_freshness_monitor' ORDER BY id DESC LIMIT 1` → ждём `alert=0`, `pull_lag_days≈6..13`, `max_pull_lag_days=14` (и конец 12-суточной серии ложных ERROR) - [ ] После следующего прогона `sber_index_pull` (13.08 05:00 UTC): у НОВЫХ периодов `fetched_at` отличается от 2026-08-06 — колонка начала мерить такт публикации Refs #2846
bot-backend added 1 commit 2026-08-12 19:27:16 +00:00
fix(tradein/sber): сторож мерит отставание загрузки, а не календарь (#2846)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m24s
b64e824e17
Порог свежести якоря был недостижим по построению. period_month — метка ПЕРВОГО
числа месяца, поэтому возрасту ≥30 уже на закрытии месяца; плюс лаг публикации
источника. За 31 сутки прямых наблюдений монитора (07-13…08-12, scrape_runs.counters)
возраст лежал в 46..76 и ни разу не опускался ниже 46 — при пороге оценщика 35.
Сторож был истинным 100% времени с рождения таблицы и нёс ноль бит: при живом
источнике и при мёртвом загрузчике он писал одно и то же.

Второй порог (монитор, 60 = 35 + запас 25) не лучше: он лежит ВНУТРИ рабочего
диапазона. Обещание миграции 212 («такт 7 ⇒ потолок возраста 53 < 60») прод
ОПРОВЕРГ — 2026-08-12 возраст 72 при полном прогоне загрузки 08-06; двенадцатые
сутки подряд ERROR при исправной загрузке. Потолок 53 держался бы, только если бы
источник публиковал строго помесячно.

Что теперь. Загрузчик тянет ВСЮ серию (limit=1000&offset=0), поэтому после прогона
с errors=0 AND upserted>0 наш max(period_month) равен максимуму источника ПО
ПОСТРОЕНИЮ. Значит вопрос «отстали ли мы» = «давно ли был последний ЗАВЕДОМО ПОЛНЫЙ
прогон», и он не зависит от возраста периода. Порог — 2 такта самой загрузки,
читается из scrape_schedules.default_params.interval_days, то есть из той же строки,
по которой планировщик считает next_run_at: разъехаться с тактом он не может.
status='done' за успех не считается — прогон id=37 имеет done при {errors: 9,
upserted: 0}. Табло спрашивается тем же порядком, что у оценщика
(SBER_COEFF_DASHBOARDS), потому что max() по таблице маскирует отставшее табло:
real_estate_deals 2026-06, dinamika-tsen-obyavlenii 2026-05.

Два порога сведены удалением: settings.sber_index_max_age_days и per-estimate
warning в estimator убраны, свежесть считает ровно одно место.

fetched_at больше не переписывается апсертом. Забор идёт всей серией, поэтому
fetched_at = now() в DO UPDATE ставил одну метку всем 639 строкам, включая период
2017-01 — как признак свежести колонка была пуста. Теперь она означает «когда
впервые увидели период», т.е. такт публикации источника станет измеримым.
Ретроспективу это не возвращает: у уже лежащих строк метка 2026-08-06 и останется.

Refs #2846
bot-backend merged commit 3cd7e0a9c4 into main 2026-08-12 19:36:21 +00:00
Author
Collaborator

Проверил три ключевых числа сам — сошлись, и моя предпосылка действительно была неверной

я утверждал факт
наблюдений возраста «за все 74 суток» 31 прямое — монитор родился 13.07 (первый прогон), раньше счётчиков нет
прогонов загрузчика 9, из них полных 8; ловушка status='done' подтверждена (id=37 с errors=9, upserted=0 отброшен)
разрыв 20 суток 07-17 → 08-06 «след отставания загрузки» ранний прогон: миграция 212 подтянула next_run_at с 08-14 на 08-06

Разрывы между полными прогонами: 14.8 · 0.6 · 1.0 · 1.0 · 1.0 · 28.0 · 20.0. Двадцативосьмисуточный равен ровно тогдашнему такту — то есть загрузчик не опоздал ни разу за всю историю.

Отсюда важное уточнение к обоснованию порога, и хорошо, что оно записано в отчёте прямо: MISSED_PULL_CYCLES = 2 — это запас над нулевой наблюдённой просрочкой, а не над измеренной. Замер отвечает «мы не опаздывали», а не «мы опаздывали столько-то». Формулировка честная, и её стоит сохранить при будущих правках порога.

Вывод про недостижимость исходного порога устоял: пол структурный (первое число месяца + лаг публикации), а не наблюдённый, и от числа наблюдений не зависит.

Что в этой правке ценно сверх заявленного

Пороги сведены удалением, а не согласованием. Два числа на один факт (35 у оценщика, 60 у монитора) не помирены, а сокращены до одного места, плюс тест держит отсутствие второго. Согласованные константы расходятся; удалённые — нет.

Двусторонность доказана на равном возрасте. test_alert_tracks_loader_not_calendar: два состояния с одинаковым возрастом 72 и разной загрузкой дают (0, 1), а на origin/main оба дают 1. Это и есть прямое доказательство, что старый сторож не умел зеленеть, — а не рассуждение о том, что он не умел.

Найден тавтологический тест на main. test_pull_cadence_leaves_margin_under_monitor_threshold считал 46 + 7 < 60 из констант и остался бы зелёным, пока прод показывает 72. Заменён на сверку фолбэка кода с тактом миграции — этот краснеет, если их разведут.

Граница названа честно. Новый сторож не ловит «источник замолчал навсегда при исправной загрузке»: такт публикации затёрт апсертом, и «не публиковал 2 месяца» неотличимо от «публикует раз в 2 месяца». Записано в докстринге модуля, а не только в отчёте, и с ориентиром на возврат — после трёх наблюдённых публикаций.

Ярлык сегмента не тронут по верной причине. Числа говорят, что прав ярлык, а комментарий кода врёт (+65% к вторичным сделкам — ряд вторички не бывает настолько выше индекса вторичных сделок того же издателя). Но починка вернула бы в оценку серию, убранную ранее, то есть изменила бы вход оценки — чего этому PR делать нельзя. Отложено верно.

Проверять после деплоя

  • counters последнего прогона sber_freshness_monitor: ждём alert=0, pull_lag_days 6…13, max_pull_lag_days=14 — и конец 12-суточной серии ложных ERROR.
  • После загрузки 13.08 05:00 UTC у новых периодов fetched_at обязан отличаться от 2026-08-06. Если он снова одинаков у всей таблицы — правка апсерта не доехала.
## Проверил три ключевых числа сам — сошлись, и моя предпосылка действительно была неверной | | я утверждал | факт | |---|---|---| | наблюдений возраста | «за все 74 суток» | **31 прямое** — монитор родился 13.07 (первый прогон), раньше счётчиков нет | | прогонов загрузчика | — | 9, из них **полных 8**; ловушка `status='done'` подтверждена (id=37 с `errors=9, upserted=0` отброшен) | | разрыв 20 суток 07-17 → 08-06 | «след отставания загрузки» | **ранний прогон**: миграция 212 подтянула `next_run_at` с 08-14 на 08-06 | Разрывы между полными прогонами: 14.8 · 0.6 · 1.0 · 1.0 · 1.0 · **28.0** · 20.0. Двадцативосьмисуточный равен ровно тогдашнему такту — то есть **загрузчик не опоздал ни разу за всю историю**. Отсюда важное уточнение к обоснованию порога, и хорошо, что оно записано в отчёте прямо: `MISSED_PULL_CYCLES = 2` — это запас над **нулевой наблюдённой просрочкой**, а не над измеренной. Замер отвечает «мы не опаздывали», а не «мы опаздывали столько-то». Формулировка честная, и её стоит сохранить при будущих правках порога. Вывод про недостижимость исходного порога устоял: пол **структурный** (первое число месяца + лаг публикации), а не наблюдённый, и от числа наблюдений не зависит. ## Что в этой правке ценно сверх заявленного **Пороги сведены удалением, а не согласованием.** Два числа на один факт (35 у оценщика, 60 у монитора) не помирены, а сокращены до одного места, плюс тест держит **отсутствие** второго. Согласованные константы расходятся; удалённые — нет. **Двусторонность доказана на равном возрасте.** `test_alert_tracks_loader_not_calendar`: два состояния с одинаковым возрастом 72 и разной загрузкой дают `(0, 1)`, а на `origin/main` оба дают `1`. Это и есть прямое доказательство, что старый сторож **не умел зеленеть**, — а не рассуждение о том, что он не умел. **Найден тавтологический тест на main.** `test_pull_cadence_leaves_margin_under_monitor_threshold` считал `46 + 7 < 60` из констант и остался бы зелёным, пока прод показывает 72. Заменён на сверку фолбэка кода с тактом миграции — этот краснеет, если их разведут. **Граница названа честно.** Новый сторож не ловит «источник замолчал навсегда при исправной загрузке»: такт публикации затёрт апсертом, и «не публиковал 2 месяца» неотличимо от «публикует раз в 2 месяца». Записано в докстринге модуля, а не только в отчёте, и с ориентиром на возврат — после трёх наблюдённых публикаций. **Ярлык сегмента не тронут по верной причине.** Числа говорят, что прав ярлык, а комментарий кода врёт (+65% к вторичным сделкам — ряд вторички не бывает настолько выше индекса вторичных сделок того же издателя). Но починка вернула бы в оценку серию, убранную ранее, то есть изменила бы **вход оценки** — чего этому PR делать нельзя. Отложено верно. ## Проверять после деплоя - `counters` последнего прогона `sber_freshness_monitor`: ждём `alert=0`, `pull_lag_days` 6…13, `max_pull_lag_days=14` — и конец 12-суточной серии ложных ERROR. - После загрузки **13.08 05:00 UTC** у новых периодов `fetched_at` обязан отличаться от 2026-08-06. Если он снова одинаков у всей таблицы — правка апсерта не доехала.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2849
No description provided.