Коридор сделок с малой выборкой честно помечен справочным #3462

Merged
bot-backend merged 2 commits from fix/3452-corridor-advisory-zone into main 2026-09-12 10:11:14 +00:00
Collaborator

Closes #3452

Что меняется для клиента

Коридор реальных сделок Росреестра показывается начиная с трёх сделок, а страховки по нему (кламп headline сверху, radius-floor снизу) включаются только с десяти. В зоне 3..9 сделок коридор на экране выглядел ровно как рабочий, хотя цену он не держал. PR #3445 (снятие предиката d.rooms) переносит в эту зону реальных клиентов — замерено 248 → 3 и 72 → 8 сделок.

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

  • обычный случай — «справочно: сделок мало (5) — коридор ориентировочный»;
  • когда похожих объявлений рядом не нашлось и вся цена посчитана по этим же сделкам — «оценка построена на этих сделках — их всего 5».

Как это сделано

  • DkpCorridor.advisory_only (app/schemas/trade_in.py) — computed-поле: count < settings.estimate_corridor_clamp_min_n. Порог живёт в одном месте, поле верно во всех конструкторах коридора автоматически — и на POST /estimate, и на GET-rehydrate.
  • Лог (app/services/estimator.py, сразу после выдачи estimate_id) — ровно одна строка на оценку, GET-rehydrate счёт не двоит.
  • _fetch_dkp_corridor отдаёт служебный ключ scope (street / city_wide): «мало сделок на улице» и «мало сделок во всём городе» — разные новости, и лог обязан их различать. В схему ответа ключ не уходит.
  • Фронт: подпись в v1-hero (чип и блок цен ДКП) и в плитке «ДКП · РОСРЕЕСТР» v2. Микрокопия — одной функцией corridorAdvisoryNote в lib/rosreestr.ts, чтобы два экрана не разъехались.

Почему текст про выборку, а не про алгоритм (правка по ревью)

Первая редакция говорила «оценку по ним не корректировали» — и это было неправдой. Порог estimate_corridor_clamp_min_n гейтит только ДВЕ страховки; третий ценовой путь коридора — гейт Tier C (#1795 шаг 3, estimator.py) — сравнивает якорь с потолком коридора без порога вообще, и коридор из пяти сделок способен уронить headline на треть (ревьюер воспроизвёл: якорь Tier C 300 000 ₽/м², с коридором 200 000 против 300 500 без него). Плюс deals-headline-fallback берёт медиану коридора начиная с трёх сделок. Утверждение о РАЗМЕРЕ ВЫБОРКИ истинно во всех достижимых состояниях — его и показываем. Докстринги advisory_only и комментарий у лога тоже перестали обещать «в цену не пошёл».

Маркер лога и как считать попадания на проде

Точный маркер: corridor_advisory_zone (уникален в кодовой базе).

Формат строки:

corridor_advisory_zone #3452: id=<uuid оценки> n=9 min_n=10 scope=street city=екатеринбург

Шаг 1 — убедиться, что чтение вообще состоялось. Контейнер называется tradein-backend (не mera-backend); при опечатке docker logs ... 2>&1 | grep -c печатает 0, и отказ читается ровно как «зона пуста». Плюс --since 24h не создаёт логов за сутки: после рестарта контейнер помнит только с момента старта.

ssh poincare 'docker inspect -f "started={{.State.StartedAt}}" tradein-backend; \
  echo -n "lines_24h="; docker logs tradein-backend --since 24h 2>&1 | wc -l'

lines_24h=0 или started= моложе суток → окно короче заявленного, число из шага 2 неполное. (Замер 12.09 в 09:35 UTC: started=2026-09-12T09:10:26Z, lines_24h=163 — то есть реально покрыты 25 минут, а не сутки.)

Шаг 2 — счёт попаданий:

ssh poincare 'docker logs tradein-backend --since 24h 2>&1 | grep -c "corridor_advisory_zone"'

Разрез по выборке (улица против расширения до города):

ssh poincare 'docker logs tradein-backend --since 24h 2>&1 | grep "corridor_advisory_zone" \
  | grep -o "scope=[a-z_]*" | sort | uniq -c'

Проверки

  • uv run python -m pytest tests/ -q5940 passed, 35 skipped, rc=0
  • uv run ruff check app tests → All checks passed, rc=0
  • uv run ruff format --check по изменённым файлам → 3 files already formatted (в репозитории есть 15 предсуществующих неотформатированных файлов — мои не в их числе)
  • фронт: npx tsc --noEmit rc=0, npx vitest run → 32 файла / 278 тестов passed, npm run build rc=0

Новый файл tests/test_3452_corridor_advisory_zone.py проверяет ФАКТ, а не только метку: коридор с потолком заведомо ниже медианы аналогов, в зоне headline остаётся 200 000 ₽/м² (кламп выключен порогом), выше порога прижимается к cap = 140 000 ₽/м². Фальсификация прогнана руками — флаг, захардкоженный в любую сторону, и снятый порог клампа роняют тесты.

🤖 Generated with Claude Code

Closes #3452 ## Что меняется для клиента Коридор реальных сделок Росреестра показывается начиная с трёх сделок, а страховки по нему (кламп headline сверху, radius-floor снизу) включаются только с десяти. В зоне 3..9 сделок коридор на экране выглядел ровно как рабочий, хотя цену он не держал. PR #3445 (снятие предиката `d.rooms`) переносит в эту зону реальных клиентов — замерено 248 → 3 и 72 → 8 сделок. Порог показа НЕ поднят (иначе клиент лишился бы информации), кламп по трём сделкам НЕ включён (он был бы хуже своего отсутствия). Вместо этого рядом с коридором теперь стоит размер его выборки: - обычный случай — **«справочно: сделок мало (5) — коридор ориентировочный»**; - когда похожих объявлений рядом не нашлось и вся цена посчитана по этим же сделкам — **«оценка построена на этих сделках — их всего 5»**. ## Как это сделано - **`DkpCorridor.advisory_only`** (`app/schemas/trade_in.py`) — computed-поле: `count < settings.estimate_corridor_clamp_min_n`. Порог живёт в одном месте, поле верно во всех конструкторах коридора автоматически — и на POST `/estimate`, и на GET-rehydrate. - **Лог** (`app/services/estimator.py`, сразу после выдачи `estimate_id`) — ровно одна строка на оценку, GET-rehydrate счёт не двоит. - **`_fetch_dkp_corridor`** отдаёт служебный ключ `scope` (`street` / `city_wide`): «мало сделок на улице» и «мало сделок во всём городе» — разные новости, и лог обязан их различать. В схему ответа ключ не уходит. - **Фронт**: подпись в v1-hero (чип и блок цен ДКП) и в плитке «ДКП · РОСРЕЕСТР» v2. Микрокопия — одной функцией `corridorAdvisoryNote` в `lib/rosreestr.ts`, чтобы два экрана не разъехались. ### Почему текст про выборку, а не про алгоритм (правка по ревью) Первая редакция говорила «оценку по ним не корректировали» — и это было неправдой. Порог `estimate_corridor_clamp_min_n` гейтит только ДВЕ страховки; третий ценовой путь коридора — гейт Tier C (`#1795 шаг 3`, `estimator.py`) — сравнивает якорь с потолком коридора **без порога вообще**, и коридор из пяти сделок способен уронить headline на треть (ревьюер воспроизвёл: якорь Tier C 300 000 ₽/м², с коридором 200 000 против 300 500 без него). Плюс deals-headline-fallback берёт медиану коридора начиная с трёх сделок. Утверждение о РАЗМЕРЕ ВЫБОРКИ истинно во всех достижимых состояниях — его и показываем. Докстринги `advisory_only` и комментарий у лога тоже перестали обещать «в цену не пошёл». ## Маркер лога и как считать попадания на проде Точный маркер: **`corridor_advisory_zone`** (уникален в кодовой базе). Формат строки: ``` corridor_advisory_zone #3452: id=<uuid оценки> n=9 min_n=10 scope=street city=екатеринбург ``` **Шаг 1 — убедиться, что чтение вообще состоялось.** Контейнер называется `tradein-backend` (не `mera-backend`); при опечатке `docker logs ... 2>&1 | grep -c` печатает `0`, и отказ читается ровно как «зона пуста». Плюс `--since 24h` не создаёт логов за сутки: после рестарта контейнер помнит только с момента старта. ``` ssh poincare 'docker inspect -f "started={{.State.StartedAt}}" tradein-backend; \ echo -n "lines_24h="; docker logs tradein-backend --since 24h 2>&1 | wc -l' ``` `lines_24h=0` или `started=` моложе суток → окно короче заявленного, число из шага 2 неполное. (Замер 12.09 в 09:35 UTC: `started=2026-09-12T09:10:26Z`, `lines_24h=163` — то есть реально покрыты 25 минут, а не сутки.) **Шаг 2 — счёт попаданий:** ``` ssh poincare 'docker logs tradein-backend --since 24h 2>&1 | grep -c "corridor_advisory_zone"' ``` **Разрез по выборке (улица против расширения до города):** ``` ssh poincare 'docker logs tradein-backend --since 24h 2>&1 | grep "corridor_advisory_zone" \ | grep -o "scope=[a-z_]*" | sort | uniq -c' ``` ## Проверки - `uv run python -m pytest tests/ -q` → **5940 passed, 35 skipped**, rc=0 - `uv run ruff check app tests` → All checks passed, rc=0 - `uv run ruff format --check` по изменённым файлам → 3 files already formatted (в репозитории есть 15 предсуществующих неотформатированных файлов — мои не в их числе) - фронт: `npx tsc --noEmit` rc=0, `npx vitest run` → 32 файла / 278 тестов passed, `npm run build` rc=0 Новый файл `tests/test_3452_corridor_advisory_zone.py` проверяет ФАКТ, а не только метку: коридор с потолком заведомо ниже медианы аналогов, в зоне headline остаётся 200 000 ₽/м² (кламп выключен порогом), выше порога прижимается к cap = 140 000 ₽/м². Фальсификация прогнана руками — флаг, захардкоженный в любую сторону, и снятый порог клампа роняют тесты. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-12 08:44:39 +00:00
fix(tradein): коридор ДКП с малым числом сделок помечен справочным
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m16s
CI Trade-In / backend-tests (pull_request) Successful in 5m32s
b71f3f9957
Показ коридора открывается с трёх сделок (DKP_CORRIDOR_CITY_WIDE_MIN_N), а обе
ценовые страховки по нему — soft-кламп headline сверху и radius-floor снизу —
включаются с десяти (estimate_corridor_clamp_min_n). В зоне n=3..9 коридор
существует, показывается и участвует в fallback-путях, но цену не держит, и на
экране это неотличимо от работающего коридора. PR #3445 (снятие предиката
d.rooms) переносит туда реальных клиентов: замерено 248 → 3 и 72 → 8 сделок.

Решение — advisory-only. Порог показа не поднят (это отняло бы у клиента
информацию), кламп по трём сделкам не включён (был бы хуже своего отсутствия),
но зона теперь названа вслух:

- DkpCorridor.advisory_only — computed-поле от count и ЕДИНСТВЕННОГО порога
  estimate_corridor_clamp_min_n, так что верно во всех конструкторах коридора
  (POST /estimate и GET-rehydrate) и не дублирует порог вторым числом;
- лог INFO с маркером corridor_advisory_zone (n, порог, scope street/city_wide,
  id оценки) — одна строка на оценку, считается за сутки одним grep -c;
- _fetch_dkp_corridor отдаёт служебный ключ scope: «мало сделок на улице» и
  «мало сделок во всём городе» — разные новости, и лог обязан их различать;
- на экране (v1 hero + плитка ДКП в v2) подпись «справочно: мало сделок —
  оценку по ним не корректировали». Подпись молчит, когда headline ПОСТРОЕН из
  этого же коридора (n_analogs = 0, deals-fallback): там показанная цена и есть
  медиана этих сделок, и подпись была бы ложью в другую сторону.

Тесты по значению (test_3452_corridor_advisory_zone.py) гоняют настоящий
estimate_quality с коридором, потолок которого заведомо ниже медианы аналогов:
в зоне headline НЕ прижат и метка стоит, выше порога — прижат к cap и метки
нет. Захардкоженный флаг в любую сторону и снятый порог клампа роняют тесты
(проверено руками).

Closes #3452

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-12 09:37:42 +00:00
fix(tradein): подпись коридора говорит про выборку, а не про алгоритм
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
CI Trade-In / backend-tests (pull_request) Successful in 5m21s
668ac40631
Ревью #3462 поймало ложь в микрокопии: «оценку по ним не корректировали»
утверждает про АЛГОРИТМ то, чего код не гарантирует. Порог
estimate_corridor_clamp_min_n гейтит только две страховки — кламп headline и
radius-floor. Третий ценовой путь, гейт Tier C (#1795 шаг 3, estimator.py),
сравнивает якорь с потолком коридора БЕЗ порога вообще: коридор из пяти сделок
там способен уронить headline на треть (воспроизведено ревьюером: якорь Tier C
300 000 ₽/м², с коридором 200 000 против 300 500 без него). Плюс
deals-headline-fallback берёт медиану коридора начиная с трёх сделок.

Формулировка переписана на утверждение о ДАННЫХ — оно истинно во всех
достижимых состояниях: «справочно: сделок мало (N) — коридор ориентировочный».

Ветка «объявлений рядом нет» (n_analogs = 0) больше не молчит: раньше там
возвращался null, и клиент не узнавал, что вся его цена стоит на трёх сделках.
Теперь — «оценка построена на этих сделках — их всего N».

Докстринги advisory_only в схеме и комментарий у лога тоже перестали обещать
«коридор в цену не пошёл»: поле значит ровно «страховки выключены».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit dfc39355f9 into main 2026-09-12 10:11:14 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3462
No description provided.