fix(tradein/estimate): отдавать фактический радиус подбора, а не выбор пользователя #2835

Merged
bot-backend merged 1 commit from fix/2632-actual-radius into main 2026-08-12 12:48:19 +00:00
Collaborator

Постановка подтвердилась ЧАСТИЧНО

Пункты 1-3 исходного #2632 закрыты в #2643: search_radius_m в схеме, круг v2 по нему, disclosure. Но на СОХРАНЁННОЙ оценке поле пустое — колонки под него нет, GET /estimate/{id} его не восстанавливал. Фронт падал на превью-радиус 1 км, то есть исходный дефект («картинка убедительнее текста») жив ровно там, где по оценке ходят чаще всего: shared-link, ?id=, перезагрузка страницы.

Живой прод, 2026-08-11, оценка 4c20518f (Советская 51), запрос изнутри контейнера:

search_radius_m: null
relaxations:     ["радиус расширен до 5000 м"]
анналогов 10, все дальше 1 км, самый дальний — 4381 м
confidence_explanation: "... (расширили радиус до 2 км из-за нехватки данных)"

Три утверждения об одном радиусе на одном экране: круг 1 км, подпись 5 км, текст 2 км.

Замер расхождения (прод, tradein_estimates)

Метрика Значение
Оценок с расстояниями у аналогов 388
Из них с аналогом дальше 1 км (вне нарисованного круга) 62 (16.0%)
Медиана максимального расстояния 787 м
p90 1572 м
Максимум 4381 м (4.4× круга)
Оценок после каскада #oblast-F (с 10.08) 19, из них 6 с расширением радиуса: 4× 2000 м, 2× 5000 м

Изменения

  1. GET восстанавливает фактический радиус из того, что реально персистится: подпись каскада «радиус расширен до N м» (точное значение, идентично POST), иначе — размах сохранённых аналогов, но не ниже DEFAULT_RADIUS_M. Нет расстояний → остаётся null (фронт падает на выбор пользователя, как раньше). Миграция не нужна. Приём не новый: v2 SourcesMap уже рисует круг по max(distance_m).
  2. Подпись каскада и её разбор — один helper (radius_relaxation_label / radius_from_relaxations), иначе строка и парсер разъедутся.
  3. confidence_explanation называет фактический радиус. Было прибито «расширили радиус до 2 км» — константа старого 1км→2км fallback'а, хотя каскад #oblast-F уходит до 3/5 км. Метка называла вход, а не результат — тот же дефект, что чинили в #2811.
  4. Ответ несёт ОБЕ величины: requested_radius_m (с чего начали) рядом с search_radius_m (чем кончили). На GET requested_radius_m остаётся null: radius_m не персистится, и подставить дефолт значило бы выдать догадку за выбор пользователя.

Фронт не трогал — v2 уже читает search_radius_m и рисует круг по нему, микрокопия причины уже есть: «Аналогов в радиусе 1 км не хватило — поиск расширен до 5 км». Правка бэкенда оживляет её на реоткрытой оценке.

Test plan

  • Новый tests/test_estimator_search_radius_2632.py (9 тестов) — красный на origin/main:
    • AssertionError: объяснение называет не тот радиус: '... (расширили радиус до 2 км из-за нехватки данных).'
    • AssertionError: сохранённая оценка отдала search_radius_m=None, а relaxations говорят ['радиус расширен до 5000 м'], самый дальний аналог 4381 м
  • Полный бэкенд-прогон: 4249 passed, 18 skipped
  • ruff check + ruff-format (pre-commit, pinned v0.7.4) — чисто
  • Post-deploy: живой POST на данных-бедный адрес (обе величины в ответе) + GET по нему же (число совпадает) + скриншот круга

Известные границы (не в этом PR)

  • Пользователь явно выбрал радиус → на реоткрытой оценке его не восстановить (radius_m не персистится). Расширения там нет по контракту #2044, но круг рисуется по превью 1 км. Нужна колонка → отдельная задача для database-expert.
  • v1 MapCard.tsx:139 рисует круг с жёстко зашитым radius: 2000. Однострочная правка на estimate.search_radius_m ?? 2000 — оставил frontend-engineer.

Refs #2632

## Постановка подтвердилась ЧАСТИЧНО Пункты 1-3 исходного #2632 закрыты в #2643: `search_radius_m` в схеме, круг v2 по нему, disclosure. **Но на СОХРАНЁННОЙ оценке поле пустое** — колонки под него нет, `GET /estimate/{id}` его не восстанавливал. Фронт падал на превью-радиус 1 км, то есть исходный дефект («картинка убедительнее текста») жив ровно там, где по оценке ходят чаще всего: shared-link, `?id=`, перезагрузка страницы. Живой прод, 2026-08-11, оценка `4c20518f` (Советская 51), запрос изнутри контейнера: ``` search_radius_m: null relaxations: ["радиус расширен до 5000 м"] анналогов 10, все дальше 1 км, самый дальний — 4381 м confidence_explanation: "... (расширили радиус до 2 км из-за нехватки данных)" ``` Три утверждения об одном радиусе на одном экране: круг 1 км, подпись 5 км, текст 2 км. ## Замер расхождения (прод, `tradein_estimates`) | Метрика | Значение | |---|---| | Оценок с расстояниями у аналогов | 388 | | Из них с аналогом дальше 1 км (вне нарисованного круга) | **62 (16.0%)** | | Медиана максимального расстояния | 787 м | | p90 | 1572 м | | Максимум | **4381 м** (4.4× круга) | | Оценок после каскада #oblast-F (с 10.08) | 19, из них 6 с расширением радиуса: 4× 2000 м, 2× 5000 м | ## Изменения 1. **GET восстанавливает фактический радиус** из того, что реально персистится: подпись каскада «радиус расширен до N м» (точное значение, идентично POST), иначе — размах сохранённых аналогов, но не ниже `DEFAULT_RADIUS_M`. Нет расстояний → остаётся `null` (фронт падает на выбор пользователя, как раньше). **Миграция не нужна.** Приём не новый: v2 `SourcesMap` уже рисует круг по `max(distance_m)`. 2. **Подпись каскада и её разбор — один helper** (`radius_relaxation_label` / `radius_from_relaxations`), иначе строка и парсер разъедутся. 3. **`confidence_explanation` называет фактический радиус.** Было прибито «расширили радиус до 2 км» — константа старого 1км→2км fallback'а, хотя каскад #oblast-F уходит до 3/5 км. Метка называла вход, а не результат — тот же дефект, что чинили в #2811. 4. **Ответ несёт ОБЕ величины**: `requested_radius_m` (с чего начали) рядом с `search_radius_m` (чем кончили). На GET `requested_radius_m` остаётся `null`: `radius_m` не персистится, и подставить дефолт значило бы выдать догадку за выбор пользователя. Фронт **не трогал** — v2 уже читает `search_radius_m` и рисует круг по нему, микрокопия причины уже есть: «Аналогов в радиусе 1 км не хватило — поиск расширен до 5 км». Правка бэкенда оживляет её на реоткрытой оценке. ## Test plan - [x] Новый `tests/test_estimator_search_radius_2632.py` (9 тестов) — **красный на origin/main**: - `AssertionError: объяснение называет не тот радиус: '... (расширили радиус до 2 км из-за нехватки данных).'` - `AssertionError: сохранённая оценка отдала search_radius_m=None, а relaxations говорят ['радиус расширен до 5000 м'], самый дальний аналог 4381 м` - [x] Полный бэкенд-прогон: **4249 passed, 18 skipped** - [x] `ruff check` + `ruff-format` (pre-commit, pinned v0.7.4) — чисто - [ ] Post-deploy: живой POST на данных-бедный адрес (обе величины в ответе) + GET по нему же (число совпадает) + скриншот круга ## Известные границы (не в этом PR) - Пользователь **явно** выбрал радиус → на реоткрытой оценке его не восстановить (`radius_m` не персистится). Расширения там нет по контракту #2044, но круг рисуется по превью 1 км. Нужна колонка → отдельная задача для database-expert. - v1 `MapCard.tsx:139` рисует круг с жёстко зашитым `radius: 2000`. Однострочная правка на `estimate.search_radius_m ?? 2000` — оставил frontend-engineer. Refs #2632
bot-backend added 1 commit 2026-08-12 12:43:35 +00:00
fix(tradein/estimate): отдавать фактический радиус подбора, а не выбор пользователя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m2s
dae3f5bc41
Сохранённая оценка отдавала search_radius_m=null, и карта рисовала круг по
превью-радиусу 1 км. Прод 2026-08-11 (оценка 4c20518f, Советская 51):
relaxations = ["радиус расширен до 5000 м"], все 10 показанных аналогов вне
круга, самый дальний — 4381 м. По всей базе: у 62 из 388 оценок с расстояниями
(16%) есть аналог дальше километра, p90 максимума 1572 м, максимум 4381 м.

- GET /estimate/{id} восстанавливает фактический радиус из того, что реально
  персистится: подпись каскада «радиус расширен до N м» (точное значение, то же,
  что вернул POST), иначе — размах сохранённых аналогов, но не ниже
  DEFAULT_RADIUS_M. Нет расстояний — остаётся null, фронт падает на выбор
  пользователя, как раньше. Колонки/миграции не требуется.
- Подпись каскада и её разбор — один helper (radius_relaxation_label /
  radius_from_relaxations), иначе строка и парсер разъедутся.
- confidence_explanation называет ФАКТИЧЕСКИЙ радиус: было прибито «расширили
  радиус до 2 км» (константа старого 1км→2км fallback'а) даже когда каскад
  #oblast-F уходил до 5 км — метка называла не результат (ср. #2811).
- Ответ несёт обе величины: requested_radius_m (с чего начали) рядом с
  search_radius_m (чем кончили). На GET requested_radius_m остаётся null —
  radius_m не персистится, подставлять дефолт значило бы выдать догадку за
  выбор пользователя.

Refs #2632
bot-backend merged commit ad190be233 into main 2026-08-12 12:48:19 +00:00
bot-backend deleted branch fix/2632-actual-radius 2026-08-12 12:48:20 +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#2835
No description provided.