fix(site-finder): метка источника весов выводится из результата резолва, а не из входа (#2811) #2817

Merged
bot-backend merged 1 commit from fix/2811-weights-source-honesty into main 2026-08-10 10:34:40 +00:00
Collaborator

Summary

/analyze ставил weights_profile.source = "profile" по условию profile_id is not None — по факту прихода параметра, а не по факту нахождения профиля. resolve_weights при промахе молча уходил вниз по лестнице приоритетов, единственный след — logger.debug, которого на проде нет. Ответ утверждал, что оценка посчитана по профилю, хотя веса были другие; опровергнуть по ответу нечем.

Воспроизведено живым запросом на проде (docker exec gendesign-backend-1 curl, участок 66:41:0204016:10), все три способа промаха:

запрос source tram_stop применённый правда
?profile_id=1 (owner не передан) profile -0.5 системные веса
?profile_id=1&profile_user_id=__system__ (чужой профиль, 1 принадлежит admin) profile -0.5 системные веса
?profile_id=999999&profile_user_id=admin (нет такого id) profile -0.4 default-профиль admin (id=1), не 999999

У профиля 1 tram_stop = -0.4, у системных — -0.5; остальные 10 категорий совпадают.

В истории прода дефект тоже есть: из 3 ранов с меткой profile один (analysis_runs.id=4000, 2026-08-07, profile_id=1, profile_user_id=NULL) посчитан системными весами.

Что сделано

  • resolve_weights возвращает ResolvedWeights(weights, source) (NamedTuple) — вызывающий не может взять веса и не взять источник; старый вызов w["school"] падает громко.
  • Запрошенный, но не применённый профиль → logger.warning с profile_id / user_id / фактическим источником. Покрывает и случай «owner не передан», где первая ветка вообще не выполнялась.
  • Ответ /analyze получил weights_profile.requested_profile_applied: True / False / None (не запрашивали). source не схлопывается в "system" — «что просили» и «что получилось» остаются разными величинами. То же поле пишется в analysis_runs.params.
  • HTTP-код НЕ менялся. profile_id для /analyze — необязательный модификатор, а не адресуемый ресурс: 404 на /parcels/{cad}/analyze уже занят «нет геометрии», а 422 превратил бы гонку «профиль удалили между показом списка и анализом» в отказ вместо честно помеченного ответа. Дефектом была метка, а не сам fallback.

Тесты

Новый test_analyze_missing_profile_is_not_labelled_profile красный на origin/main:

E  AssertionError: ?profile_id=999999: применились системные веса, а метка
   source='profile' — ответ утверждает то, чего не было (#2811)
E  assert 'profile' != 'profile'

Плюс: обратный случай (профиль найден → source='profile', флаг True), unit-тест на сценарий «profile_id без owner» (проверяет warning), unit-тест на «промах → user_default, а не profile» (там веса не системные, по значениям подмена не видна вовсе).

Кто читает метку

Полный grep: weights_profile.source нигде не строит видимый пользователю текст. Фронт её не рендерит (три упоминания — комментарии-предупреждения «врёт», #2782/#2810), в §19-allowlist чата её нет, PDF/DOCX/экспортёры не читают. Единственные потребители — сырой ответ API и analysis_runs.params.weights_source. Видимых изменений в UI не будет; после этого PR фронтовые комментарии становятся неактуальны (отдельный follow-up, фронт не трогал).

Test plan

  • pytest tests/test_weight_profiles.py — 16 passed
  • pytest tests/api/v1/test_analyze_inline_weights.py — 8 passed
  • pytest tests/api/v1/test_run_history_and_response_contract.py — 10 passed
  • красный прогон нового теста на origin/main
  • post-deploy: те же три запроса на проде → source = system / system / user_default, requested_profile_applied=false, warning в docker logs

Refs #2811

## Summary `/analyze` ставил `weights_profile.source = "profile"` по условию `profile_id is not None` — по факту прихода параметра, а не по факту нахождения профиля. `resolve_weights` при промахе молча уходил вниз по лестнице приоритетов, единственный след — `logger.debug`, которого на проде нет. Ответ утверждал, что оценка посчитана по профилю, хотя веса были другие; опровергнуть по ответу нечем. **Воспроизведено живым запросом на проде** (`docker exec gendesign-backend-1 curl`, участок `66:41:0204016:10`), все три способа промаха: | запрос | `source` | `tram_stop` применённый | правда | |---|---|---|---| | `?profile_id=1` (owner не передан) | `profile` | `-0.5` | системные веса | | `?profile_id=1&profile_user_id=__system__` (чужой профиль, 1 принадлежит `admin`) | `profile` | `-0.5` | системные веса | | `?profile_id=999999&profile_user_id=admin` (нет такого id) | `profile` | `-0.4` | **default-профиль admin (id=1)**, не 999999 | У профиля 1 `tram_stop = -0.4`, у системных — `-0.5`; остальные 10 категорий совпадают. В истории прода дефект тоже есть: из 3 ранов с меткой `profile` один (`analysis_runs.id=4000`, 2026-08-07, `profile_id=1`, `profile_user_id=NULL`) посчитан системными весами. ## Что сделано - `resolve_weights` возвращает `ResolvedWeights(weights, source)` (NamedTuple) — вызывающий не может взять веса и не взять источник; старый вызов `w["school"]` падает громко. - Запрошенный, но не применённый профиль → `logger.warning` с `profile_id` / `user_id` / фактическим источником. Покрывает и случай «owner не передан», где первая ветка вообще не выполнялась. - Ответ `/analyze` получил `weights_profile.requested_profile_applied`: `True` / `False` / `None` (не запрашивали). `source` не схлопывается в `"system"` — «что просили» и «что получилось» остаются разными величинами. То же поле пишется в `analysis_runs.params`. - **HTTP-код НЕ менялся.** `profile_id` для `/analyze` — необязательный модификатор, а не адресуемый ресурс: 404 на `/parcels/{cad}/analyze` уже занят «нет геометрии», а 422 превратил бы гонку «профиль удалили между показом списка и анализом» в отказ вместо честно помеченного ответа. Дефектом была метка, а не сам fallback. ## Тесты Новый `test_analyze_missing_profile_is_not_labelled_profile` **красный на `origin/main`**: ``` E AssertionError: ?profile_id=999999: применились системные веса, а метка source='profile' — ответ утверждает то, чего не было (#2811) E assert 'profile' != 'profile' ``` Плюс: обратный случай (профиль найден → `source='profile'`, флаг `True`), unit-тест на сценарий «profile_id без owner» (проверяет warning), unit-тест на «промах → user_default, а не profile» (там веса не системные, по значениям подмена не видна вовсе). ## Кто читает метку Полный grep: `weights_profile.source` **нигде не строит видимый пользователю текст**. Фронт её не рендерит (три упоминания — комментарии-предупреждения «врёт», #2782/#2810), в §19-allowlist чата её нет, PDF/DOCX/экспортёры не читают. Единственные потребители — сырой ответ API и `analysis_runs.params.weights_source`. Видимых изменений в UI не будет; после этого PR фронтовые комментарии становятся неактуальны (отдельный follow-up, фронт не трогал). ## Test plan - [x] `pytest tests/test_weight_profiles.py` — 16 passed - [x] `pytest tests/api/v1/test_analyze_inline_weights.py` — 8 passed - [x] `pytest tests/api/v1/test_run_history_and_response_contract.py` — 10 passed - [x] красный прогон нового теста на `origin/main` - [ ] post-deploy: те же три запроса на проде → `source` = `system` / `system` / `user_default`, `requested_profile_applied=false`, warning в `docker logs` Refs #2811
bot-backend added 1 commit 2026-08-10 10:15:53 +00:00
fix(site-finder): метка источника весов выводится из результата резолва, а не из входа (#2811)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m18s
CI / backend-tests (pull_request) Successful in 16m50s
34d5fb76b7
`/analyze` ставил `weights_profile.source = "profile"` по условию
`profile_id is not None` — то есть по факту, что параметр пришёл. Нашёлся ли
профиль, никто не спрашивал, а `resolve_weights` при промахе молча уходил вниз
по лестнице приоритетов (единственный след — `logger.debug`, которого на проде
нет). Ответ утверждал, что оценка посчитана по профилю, хотя веса были другие,
и опровергнуть это по ответу было нечем.

Воспроизведено живым запросом на проде (все три способа промаха: owner не
передан, чужой профиль, несуществующий id) и найдено в истории: ран
analysis_runs #4000 от 2026-08-07 — source='profile', profile_id=1, при
tram_stop=-0.5 (системный; у профиля 1 он -0.4).

- `resolve_weights` возвращает `ResolvedWeights(weights, source)` — вызывающий
  не может взять веса и не взять источник.
- Запрошенный, но не применённый профиль пишется `logger.warning` с id.
- В ответе появился `weights_profile.requested_profile_applied` (True/False/None):
  «что просили» и «что получилось» остаются разными величинами, source не
  схлопывается в "system".
- HTTP-код не менялся: profile_id для /analyze — необязательный модификатор,
  а не адресуемый ресурс; 404/422 превратил бы гонку «профиль удалили между
  списком и анализом» в отказ вместо честно помеченного ответа.

Refs #2811
bot-backend merged commit 1307d55da6 into main 2026-08-10 10:34:40 +00:00
bot-backend deleted branch fix/2811-weights-source-honesty 2026-08-10 10:34:40 +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#2817
No description provided.