fix(freshness): output-floor on cycle-SUM over fresh_days window (#1945, #1947) #1948

Merged
bot-backend merged 1 commit from feat/freshness-output-floor into main 2026-06-27 07:50:04 +00:00
Collaborator

Зачем

Прод-проверка #1947 (zero-output upd_7d==0) показала, что чек не ловит реальную поломку и ложно-срабатывает:

  1. Пропуск: kn_flats latest done-прогон дал flats_count=9 (не ровно 0) → ==0 мимо. А 9 против здоровых ~376k — это поломка (#1945, тихо 5+ недель: ground-truth domrf_kn_flats latest snapshot 2026-06-22=9 vs 2026-05-17=376604).
  2. Ложный алёрт: окно 7d, а kn_flats.fresh_days=8 (недельная каденция) → здоровый прогон возрастом 7–8d даёт upd_7d=0 при status=ok → ложный downgrade ~1 день/неделю.

Первый редизайн (single-newest last_success_work) адверсариал-ревью (4 линзы) зарубило блокером: логический цикл загрузки — это несколько done-прогонов (objective пишет строку на каждую из 4 групп group_name; kn — на region_codes + resume-хвост), поэтому одиночный последний прогон искажает картину. Прод подтвердил: objective single-newest=324570 (повезло, что Свердловская финишировала последней; Челябинск=68387), kn_flats single-newest=9 (resume-хвост).

Что

Floor сравнивается с SUM(work_col) по всем done-прогонам цикла в окне fresh_days (а не одиночный прогон и не хардкод-7d):

SUM(COALESCE(work_col, 0)) FILTER (
    WHERE status = 'done'
      AND COALESCE(finished_at, started) > now() - make_interval(secs => fresh_days * 86400)
) AS recent_output
  • Cycle-SUM агрегирует весь цикл → магнитуды совпадают со здоровыми (прод: objective SUM 7d=946264, kn_flats SUM 8d=9).
  • Окно = fresh_days → здоровый недельный прогон 7–8d остаётся в окне → нет aging-FP (фикс #1947 сохранён).
  • make_interval(secs => fresh_days*86400) (param days в PG — int; secs — double, future-proof для дробного fresh_days). COALESCE(work_col,0) — NULL-счётчик = 0. Без ::-каста.

Даунгрейд: alert_on_zero_output and timestamp_col is None and status=='ok' and recent_output is not None and recent_output < min_output_rows (строгое <).

Реестр (floors сохранены): kn_flats=50000 (healthy ~376k, broken sum ≤3670), objective=1000 (SUM 7d ~946k, наименьшая группа 68k). Комменты переписаны на cycle-sum basis + KNOWN LIMITATION по kn_flats (zombie-resume малый хвост без полного прогона в окне → транзиентный one-cycle ложняк, самозаживает).

Безопасность / контракт

  • Аддитивно: новый nullable ключ recent_output (int|None) в каждом source-объекте; reason уже был. Эндпоинт dict[str, Any] (нет response_model) → ключ проходит, фронт /freshness не консьюмит, openapi-codegen не задет (ревью frontend-contract: clean).
  • Адверсариал-ревью: SQL/PG-семантика clean, frontend-contract clean, threshold/FP — блокер устранён этим редизайном, тесты — concerns закрыты.
  • ruff , SQL ::-cast grep пусто, pytest -k "freshness or admin_scrape"19 passed, 1 skipped; консьюмер test_scrape_freshness_check5 passed (status=failed → алёрт; critical=True → error-level GlitchTip).

Migration не нужна (чистый код). После деплоя GET /admin/scrape/freshness должен показать kn_flats с recent_output=9, status=failed, reason=low-output… → ловит #1945, который тихо шёл 5+ недель.

## Зачем Прод-проверка #1947 (zero-output `upd_7d==0`) показала, что чек **не ловит реальную поломку** и **ложно-срабатывает**: 1. **Пропуск:** `kn_flats` latest done-прогон дал `flats_count=9` (не ровно 0) → `==0` мимо. А 9 против здоровых ~376k — это поломка (#1945, тихо 5+ недель: ground-truth `domrf_kn_flats` latest snapshot **2026-06-22=9** vs **2026-05-17=376604**). 2. **Ложный алёрт:** окно 7d, а `kn_flats.fresh_days=8` (недельная каденция) → здоровый прогон возрастом 7–8d даёт `upd_7d=0` при `status=ok` → ложный downgrade ~1 день/неделю. Первый редизайн (single-newest `last_success_work`) **адверсариал-ревью (4 линзы) зарубило блокером**: логический цикл загрузки — это *несколько* done-прогонов (objective пишет строку на каждую из 4 групп `group_name`; kn — на `region_codes` + resume-хвост), поэтому одиночный последний прогон искажает картину. Прод подтвердил: objective single-newest=324570 (повезло, что Свердловская финишировала последней; Челябинск=68387), kn_flats single-newest=9 (resume-хвост). ## Что Floor сравнивается с **`SUM(work_col)` по всем done-прогонам цикла в окне `fresh_days`** (а не одиночный прогон и не хардкод-7d): ```sql SUM(COALESCE(work_col, 0)) FILTER ( WHERE status = 'done' AND COALESCE(finished_at, started) > now() - make_interval(secs => fresh_days * 86400) ) AS recent_output ``` - **Cycle-SUM** агрегирует весь цикл → магнитуды совпадают со здоровыми (прод: objective `SUM 7d=946264`, kn_flats `SUM 8d=9`). - **Окно = `fresh_days`** → здоровый недельный прогон 7–8d остаётся в окне → нет aging-FP (фикс #1947 сохранён). - `make_interval(secs => fresh_days*86400)` (param `days` в PG — int; `secs` — double, future-proof для дробного fresh_days). `COALESCE(work_col,0)` — NULL-счётчик = 0. Без `::`-каста. Даунгрейд: `alert_on_zero_output and timestamp_col is None and status=='ok' and recent_output is not None and recent_output < min_output_rows` (строгое `<`). **Реестр (floors сохранены):** `kn_flats=50000` (healthy ~376k, broken sum ≤3670), `objective=1000` (SUM 7d ~946k, наименьшая группа 68k). Комменты переписаны на cycle-sum basis + KNOWN LIMITATION по kn_flats (zombie-resume малый хвост без полного прогона в окне → транзиентный one-cycle ложняк, самозаживает). ## Безопасность / контракт - **Аддитивно**: новый nullable ключ `recent_output` (int|None) в каждом source-объекте; `reason` уже был. Эндпоинт `dict[str, Any]` (нет response_model) → ключ проходит, фронт `/freshness` не консьюмит, openapi-codegen не задет (ревью frontend-contract: ✅ clean). - Адверсариал-ревью: SQL/PG-семантика ✅ clean, frontend-contract ✅ clean, threshold/FP — блокер устранён этим редизайном, тесты — concerns закрыты. - ruff ✅, SQL `::`-cast grep пусто, `pytest -k "freshness or admin_scrape"` → **19 passed, 1 skipped**; консьюмер `test_scrape_freshness_check` → **5 passed** (status=failed → алёрт; critical=True → error-level GlitchTip). Migration не нужна (чистый код). После деплоя `GET /admin/scrape/freshness` должен показать `kn_flats` с `recent_output=9, status=failed, reason=low-output…` → ловит #1945, который тихо шёл 5+ недель.
bot-backend added 1 commit 2026-06-27 07:37:35 +00:00
fix(freshness): output-floor on cycle-SUM over fresh_days window (#1945, #1947)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m45s
CI / backend-tests (pull_request) Successful in 11m57s
c646a71001
Adversarial review found the single-newest last_success_work basis fragile:
a load cycle = MULTIPLE done-runs (objective per group_name, kn per region +
resume), so the newest run alone misrepresents the cycle. Replace with
SUM(work_col) FILTER(done) over the source's own fresh_days window.

- recent_output = SUM(COALESCE(work_col,0)) FILTER done in fresh_days window
  (make_interval secs => fresh_days*86400; no :: cast; NULL-counter -> 0)
- downgrade when status==ok and recent_output < min_output_rows (strict <)
- registry floors kept: kn_flats=50000 (healthy ~376k, broken sum <=3670),
  objective=1000 (SUM 7d ~946k); comments rewritten to cycle-sum basis +
  kn_flats zombie-resume KNOWN LIMITATION
- window=fresh_days keeps the #1947 aging-FP fix (7-8d run stays in window)
- tests: low-output-cycle->failed, above-floor->ok, boundary==floor->ok,
  weekly-aging->ok, age-stale precedence, registry floors

Verified on prod: domrf_kn_flats latest snapshot 2026-06-22=9 flats vs healthy
2026-05-17=376604 (broken 5+ weeks); objective SUM(rows_lots,7d)=946264.
bot-backend merged commit d87641ac29 into main 2026-06-27 07:50:04 +00:00
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#1948
No description provided.