Ревью PR #3566: обещание таймера, убранное из beat, осталось в тексте
HTTP 400 эндпоинтов /admin/scrape/kn-catalog-objects и /kn-catalog-flats.
Отказ предлагал передать i_understand_waf_risk=true, «если WAF cooldown
прошёл», то есть подсказывал оператору обойти блокировку, которую ожидание
не снимает (зонды 20.08-01.09: StormWall, «Доступ заблокирован [403]»).
Теперь отказ называет StormWall и реальное условие: прокси и kn-прогон,
принятый по числу строк (#3307). Константа переименована в
_DOMRF_BLOCK_GUARD_MSG. Докстринги эндпоинтов и задач
scrape_kn_catalog_flats/objects больше не говорят про WAF cooldown и про
«вторник 04:00 UTC» у выключенной записи с расписанием в МСК.
api-types.ts перегенерирован как в CI-гейте openapi-codegen-check,
изменились только два докстринга.
Тесты отказа проверяют значение detail: #3307 есть, cooldown нет.
Поведение guard'а (400 без флага, задача не ставится) не менялось.
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>
Ревью #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>
Регекс номера требовал суффикс областных актов (-ПП/-ПА/-РП/-ПГ/-ГП/-МО),
а у постановлений Администрации Екатеринбурга его нет: на проде act_number
пуст у всех 27 строк (17.09.2026).
- izyatie_ocr: номер берётся у того же акта, чью дату выбирает
_extract_act_date (вплотную после «от DD.MM.YYYY», иначе перед ней).
Первое «№» в теле — «Решение Думы № 60/1» или «Приказ № 746-П».
- page_reservation_parser: суффикс необязателен; номер с суффиксом не из
списка («218-ФЗ», «746-П», «60/1») отбрасывается целиком, а не обрезается.
- izyatie_ocr_ingest: перед записью удаляется прежний разбор того же
участка из того же документа с другим номером. act_number в ключе
конфликта, без этого прогон положил бы 27 строк с номером рядом с 27
строками без номера.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Комментарии у закомментированных записей scrape-kn-catalog-objects-weekly и
scrape-kn-catalog-flats-weekly обещали «возврат после cooldown 24-48h
(проверить через targeted test)». Тест проведён 20.08, 27.08 и 01.09:
кулдауна нет (kn-прогоны 29-33 шли в июне, уже после «бана» 24.05), с 01.09
наш.дом.рф за StormWall отдаёт IP Poincare «Доступ заблокирован [403]».
Комментарии теперь называют факт и условие включения (прокси и принятый
kn-прогон, #3307).
Решение больше не живёт только в комментарии: тест проверяет по значению
(по task, не по ключу), что ни одна из двух задач не попала в собранное
beat-расписание, и в тексте падения ведёт в #3307.
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>
Тема форума для уведомлений бэкапов задаётся только env-файлом на хосте, а в
образцах её не было вовсе. На проде она задана, но не та: 158 («алерты») в
/opt/gendesign/secrets/backup-notify.env и forgejo-backup.env на Beget и в
/etc/default/gendesign-backup на Poincare. По решению #3163 инфраструктура идёт
в 245 «Metrics». Значение на хостах этот коммит не меняет.
- ops/gendesign-backup*.default.example: строка #TELEGRAM_TOPIC_ID=245 с
причиной и ловушкой: тема обязана лежать в одном файле с токеном и чатом,
иначе notify() её не прочитает.
- ops/crontab-beget.cron сверен с живым crontab Beget: сторожа и бэкап волта
получают BACKUP_ENV_FILE=/opt/gendesign/secrets/backup-notify.env. Без него
переустановка crontab из репозитория глушила бы алерты бэкапов на Beget.
- ops/uptime-healthcheck.sh и его образец удалены: скрипт не запущен ни на
одном хосте (crontab, cron.d, таймеры), доступность сторожат uptime-мониторы
GlitchTip на Beget (gendsgn.ru, /health, meraocenka.ru — раз в 60 с).
Тест исполняет настоящий check-backup-staleness.sh с образцом, заполненным
по инструкции, и проверяет адрес в вызове curl: message_thread_id=245.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
queries.yml смонтирован трём экспортёрам одним файлом и читается только при
старте, а агентские джобы деплоя пересоздавали лишь alloy: правка ложилась на
диск новым инодом, экспортёры продолжали отдавать старые запросы при зелёном
деплое. Разрыв латентный — 17.09 иноды хоста и контейнеров совпадают
(Poincare 5112170, Beget 569352).
agent-apps и agent-infra после подъёма сверяют инод queries.yml у своих
экспортёров через ops/metrics/recreate-stale-mount.sh и пересоздают только при
расхождении, под гейтом профиля. Гейт пофайловых маунтов теперь читает и
docker-compose.metrics-agent.yml (сервис → джоба по профилю), а ci.yml
запускает backend-тесты на правку этого файла.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хвосты #3467, не попавшие в main вместе с #3476:
- prometheus.yml смонтирован одним файлом: после git reset --hard reload
перечитывал СТАРЫЙ инод с rc=0 и новым lastConfigTime (стенд
prom/prometheus:v3.1.0, 17.09). Комментарий в деплое утверждал обратное.
Теперь promtool проверяет файлы С ДИСКА одноразовым контейнером, а при
расхождении инода контейнер пересоздаётся до reload.
- loki-config.yml — тот же пофайловый маунт, перезагрузки у Loki нет:
пересоздание при расхождении инода.
- Датасорсы Grafana применяются только при старте: POST
/api/admin/provisioning/datasources/reload, отказ роняет деплой.
Общий шаг — ops/metrics/recreate-stale-mount.sh: пересоздаёт только при
расхождении инода и перепроверяет после; тесты исполняют его с подставным
docker. Гейт берёт пофайловые маунты из docker-compose.metrics.yml.
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>
Пять булевых флагов кластера сняты ещё в #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>
Мёртвый или зависший tradein-backend не давал ни одного сигнала вне Poincare.
GlitchTip-монитор 6 смотрит на лэндинг meraocenka.ru, а его отдаёт фронт из
пререндер-кэша (x-nextjs-cache: HIT) — при мёртвом бэкенде там 200. Монитор 2
(gendsgn.ru/health) проверяет бэкенд Птицы. AppHighErrorRate и
AppHighLatencyP95 считают метрики самого бэкенда и при его смерти молчат.
Новое правило MeraBackendDown: up{job="app",app="mera"} == 0 или серия
пропала, 5 минут, severity critical + host apps — маршрут telegram-clients,
тема 158. Считается и доставляется на Beget. Порог 5 минут откалиброван по
истории up за 26.08–17.09: 140 провалов, самый длинный — 4 нулевые точки
(~2 минуты, окна деплоя).
Юнит-тесты promtool: мёртвый бэкенд — тревога; окно деплоя и упавшая Птица —
тишина; цель пропала из скрейпа — тревога.
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>
Единственный переключатель estimate_quarter_index_enabled снят ещё в #2475.
Оставшиеся шесть числовых полей Settings (min_n_deals 10, match_skip_ratio 0.6,
max_for_small_n 2.0, small_n_threshold 50, factor_min 0.6, factor_max 1.8)
перенесены в estimator.py константами QUARTER_* с теми же значениями и
комментариями #764/#859; бэктест берёт QUARTER_INDEX_MIN_N_DEALS из модуля.
На проде 17.09 все шесть равны дефолтам, ENV-оверрайдов нет.
Замороженная фикстура квартальный индекс не применяет ни разу (0 из 1600
сделок), а поведенческие тесты не краснеют при подмене трёх порогов из шести,
поэтому добавлен tests/test_1970_estimator_constants.py: боевые значения,
снятые с прода, и проверка, что поле не вернулось в Settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Флаг estimate_dedup_analogs_enabled снят: кросс-source дедуп работает всегда.
На проде 17.09 флаг = True во всех трёх контейнерах (backend/scraper/tgbot),
ENV-оверрайда нет; фикстура бэктеста захвачена с True, поэтому пин флага в
реплее и monkeypatch в гейте больше не нужны.
Числовые пороги estimate_wide_corridor_threshold и три
estimate_manual_review_* перенесены в estimator.py константами модуля с
прежними значениями (1.2 / 20 000 000 / 1.9 / 250 000). _manual_review больше
не принимает settings. Осиротевшие комментарии Settings к уже снятым в #2475
флагам (#1871 P1.2, P2 radius-dedup) удалены.
Тесты: OFF-тест дедупа удалён; два теста, пинившие дедуп OFF ради изоляции,
получили разные площади у аналогов (разные физлоты). Регрессионный гейт и
реплей бэктеста по сделкам (1600, из них 615 радиусных) побитово те же.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#1971, два невыполненных пункта DoD.
1. Уведомления не было. Заявка с результата оценки только ложилась в
trade_in_leads (docstring прямо называл уведомление «вне scope»). На проде
17.09: 4 заявки, notified_at пуст у всех, последняя 12.07. Теперь после ответа
клиенту фоновая задача шлёт сообщение в support-топик тем же ботом, что и
веб-чат поддержки, и при успехе ставит notified_at. Отказ Telegram не меняет ни
ответ (200), ни сохранённый лид — только лог. Телефона в сообщении нет: копию в
Telegram не стирает механизм удаления ПДн, поэтому туда идут id заявки,
пользователь и id оценки.
2. Доли заявок от оценок не было нигде. Панель на продуктовом дашборде: лиды за
7 суток / успешные оценки за 7 суток (знаменатель — только outcome=ok: форма
заявки показывается только при посчитанной оценке). Выражение проверено на
боевом Prometheus 17.09: 0 / 91.008 = 0.
Closes#1971
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Раунды игры — строки витрины, а витрина с 12.09 отобрана по ошибке МЕРЫ
(полоса -5..+20 %). Плитка «ошибка МЕРЫ на этих же квартирах» выходила без
оговорки и читалась как точность расчёта: на проде 17.09 медиана |ошибки|
двадцати строк 6,5 %. Лента и таблица «Точность» полосу называли, игра — нет.
Теперь итог печатает медиану по всей сверке (landing-facts) и, если все
строки витрины лежат в полосе (allWithinBand, как у ленты и таблицы),
говорит, что квартиры из отобранной полосы, а не из всей сверки.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обработчик landing_showcase_deals безусловно ставил прогону done. У счётчиков
витрины (considered/eligible/written) нет результатного ключа кита, поэтому
сводка просроченных судит её только по статусу: прогон с written=0 обнулял
часы свежести так же, как удачный, а страница тем временем теряла таблицу.
Теперь written=0 — mark_failed с причиной и logger.error. Тест идёт путём
сводки: обработчик -> freshness_rows -> stale_sources; пустой последний
прогон при старом непустом даёт витрину в тревоге, непустой — нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
После отправки оценки freshResult оставался выставленным, и клик по строке
истории ничего не менял на экране: useEstimate получал null (восстановление
по id вообще не запускалось), а estimate и currentEstimateId брались из
только что посчитанного результата. onSelectEstimate теперь сбрасывает
freshResult так же, как это делает handleNew.
Тест рендерит страницу с заглушками хуков: submit, затем клик по другой
строке истории — в useEstimate уходит id строки, HeroBar и форма получают
её id и адрес. Без правки тест красный (useEstimate вызван с null).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Две тревоги шумели в канале каждую ночь и каждые три часа, обе врали текстом.
NoActiveCeleryWorkers горела с 12.09 без перерыва при живом воркере
(`Up 29 hours`, 15 задач за 10 минут). Экспортёр не поднимался ни разу:
тега danihodovic/celery-exporter:0.13.0 на Docker Hub не существует
(`pull` → `not found`, логи джобы agent-apps 12.09 и 16.09), а под
`set -e` упавший pull обрывал ВСЮ джобу — с 12.09 на Poincare не доезжало
ничего из агента метрик: ни этот экспортёр, ни redis-exporter, ни новый
конфиг Alloy. Второй дефект за первым: флага `--queue` в 0.12.2 нет,
опция зовётся `--queues` — с одним исправленным тегом экспортёр упал бы
при старте. Тег и флаг сверены с исходником (src/cli.py, src/exporter.py)
и пробой на боевом брокере: временный контейнер 0.12.2 отдал
`celery_worker_up{hostname="687f9212bebb"} 1.0`. Адрес брокера не тронут —
алиас `gendesign-redis` на сети gendesign_shared существует и резолвится.
Правило разделено на две новости. NoActiveCeleryWorkers — только когда
экспортёр жив, а воркеров нет. QueueExporterDown (warning) — когда не
отвечает сам экспортёр, с текстом «авария наблюдаемости, не продукта».
Заодно прежняя ветка `count(celery_worker_up == 1) == 0` не сработала бы
никогда: count() от пустого вектора пуст, мёртвый воркер она пропускала.
ContainerNearMemoryLimit на tradein-postgres: working_set включает активный
кэш страниц, ночные сканы поднимают его к потолку. Анонимная память за
неделю 11–170 МБ из 3 ГБ, oom_kill = 0 за 21 сутки — «дальше OOM-kill» был
ложью. Добавлено второе условие: анонимная память > 50 % лимита.
Калибровка ЭТИМ выражением по 14 суткам истории: tradein-postgres 5 → 0
эпизодов, tradein-browser 2 → 2 (06.09 и 12.09, rss до 95 % — настоящие).
Юнит-тесты правил (ops/metrics/prometheus/tests/infra_test.yml, 6 случаев,
каждый уже случался на проде) исполняются деплоем перед reload. На старых
правилах promtool test rules → rc=1 (6 расхождений), на новых → rc=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>