fix(tradein/v2): канонический sources_used — стабильный счётчик «ИСТОЧНИКОВ X/7» (#2087 M1) #2144

Merged
lekss361 merged 1 commit from fix/tradein-v2-source-counter-canonical into main 2026-07-02 14:31:16 +00:00
Collaborator

Summary

  • Единый helper estimator._canonical_sources(analogs, valuation_flags) — источник правды для sources_used, зовётся идентично на POST (estimate_quality) и GET-rehydrate (get_estimate) из ТЕХ ЖЕ persisted analogs + оценочных флагов. Один estimate_id → идентичный sources_used на POST/GET/reload.
  • Root cause: POST брал sources_used из top-N analogs_lots, GET возвращал persisted-колонку (радиусный набор + quarter_index), а source_counts на GET считался из persisted analogs → источник (напр. cian) мог быть в source_counts, но не в sources_used; счётчик «ИСТОЧНИКОВ X/7» прыгал 4→5 между POST-ответом и reload(GET).
  • source_counts на POST теперь тоже из analogs_lots (не полной metadata-выборки) → инвариант source_counts.keys()sources_used на POST и GET.
  • POST персистит канонический sources_used в колонку (history/PDF консистентны для новых строк); GET рехайдрейтит его тем же helper'ом — чинит и СТАРЫЕ строки (листинговая часть пересобирается из analogs, quarter_index/радиусный шум отбрасывается фильтром оценочных флагов).
  • Оценочные флаги (avito_imv/yandex_valuation/cian_valuation) персистятся в колонке sources_used и читаются оттуда на GET — реконструкция не требуется.
  • FE/mappers.ts не тронуты (математика верна — так решено в #2087).

Repro (estimate 451de30b, прод, docker exec tradein-backend)

  • До: sources_used=[avito, avito_imv, domklik, yandex] (4/7), source_counts={avito:1, cian:1, domklik:6, yandex:2}cian в counts, отсутствует в sources_used (рассинхрон).
  • После (тот же helper на persisted данных): sources_used=[avito, avito_imv, cian, domklik, yandex] (5/7), source_counts.keys()sources_used.

Test plan

  • ruff check app/ — clean
  • pytest tests/ -q — 2746 passed, 4 skipped, 1 pre-existing fail (test_search_api::test_search_cache_hit, auth 401 — не связан, падает на base main)
  • Новые unit (test_estimator_source_metrics_2043.py): детерминизм sources_used при одинаковых analogs+флагах; инвариант counts.keys()sources_used; листинговый источник в analogs ⇒ в sources_used; фильтр не-канонических флагов (quarter_index/листинговые метки в колонке не протекают)
  • post-deploy smoke: GET /estimate/451de30bsources_used содержит cian, совпадает по ключам с source_counts

Part of #2087 (M1). Issue НЕ закрывается — остаются M2/M3.

🤖 Generated with Claude Code

## Summary - Единый helper `estimator._canonical_sources(analogs, valuation_flags)` — источник правды для `sources_used`, зовётся идентично на POST (`estimate_quality`) и GET-rehydrate (`get_estimate`) из ТЕХ ЖЕ persisted `analogs` + оценочных флагов. Один `estimate_id` → идентичный `sources_used` на POST/GET/reload. - **Root cause:** POST брал `sources_used` из top-N `analogs_lots`, GET возвращал persisted-колонку (радиусный набор + `quarter_index`), а `source_counts` на GET считался из persisted `analogs` → источник (напр. `cian`) мог быть в `source_counts`, но не в `sources_used`; счётчик «ИСТОЧНИКОВ X/7» прыгал 4→5 между POST-ответом и reload(GET). - `source_counts` на POST теперь тоже из `analogs_lots` (не полной metadata-выборки) → инвариант `source_counts.keys()` ⊆ `sources_used` на POST и GET. - POST персистит канонический `sources_used` в колонку (history/PDF консистентны для новых строк); GET рехайдрейтит его тем же helper'ом — чинит и **СТАРЫЕ** строки (листинговая часть пересобирается из `analogs`, `quarter_index`/радиусный шум отбрасывается фильтром оценочных флагов). - Оценочные флаги (`avito_imv`/`yandex_valuation`/`cian_valuation`) персистятся в колонке `sources_used` и читаются оттуда на GET — реконструкция не требуется. - FE/`mappers.ts` не тронуты (математика верна — так решено в #2087). ## Repro (estimate `451de30b`, прод, `docker exec tradein-backend`) - **До:** `sources_used=[avito, avito_imv, domklik, yandex]` (4/7), `source_counts={avito:1, cian:1, domklik:6, yandex:2}` → `cian` в counts, отсутствует в `sources_used` (рассинхрон). - **После (тот же helper на persisted данных):** `sources_used=[avito, avito_imv, cian, domklik, yandex]` (5/7), `source_counts.keys()` ⊆ `sources_used`. ## Test plan - [x] `ruff check app/` — clean - [x] `pytest tests/ -q` — 2746 passed, 4 skipped, 1 pre-existing fail (`test_search_api::test_search_cache_hit`, auth 401 — не связан, падает на base main) - [x] Новые unit (`test_estimator_source_metrics_2043.py`): детерминизм `sources_used` при одинаковых analogs+флагах; инвариант `counts.keys()` ⊆ `sources_used`; листинговый источник в analogs ⇒ в `sources_used`; фильтр не-канонических флагов (`quarter_index`/листинговые метки в колонке не протекают) - [ ] post-deploy smoke: GET `/estimate/451de30b` → `sources_used` содержит `cian`, совпадает по ключам с `source_counts` Part of #2087 (M1). Issue НЕ закрывается — остаются M2/M3. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-07-02 14:23:36 +00:00
fix(tradein/v2): canonical sources_used — kill «ИСТОЧНИКОВ X/7» POST↔GET desync
All checks were successful
CI / changes (pull_request) Successful in 7s
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
18b22c9df8
Единый helper estimator._canonical_sources(analogs, valuation_flags) — источник
правды для sources_used, зовётся идентично на POST (estimate_quality) и
GET-rehydrate (get_estimate) из ТЕХ ЖЕ persisted analogs + оценочных флагов.

Root cause: три расходящиеся деривации — POST брал sources_used из top-N
analogs_lots, GET возвращал persisted-колонку (радиусный набор + quarter_index),
source_counts на GET считался из persisted analogs. Источник (напр. cian) мог
попасть в source_counts, но не в sources_used → счётчик «X/7» прыгал 4→5 между
POST-ответом и reload(GET).

- sources_used = {листинговые из persisted analogs} ∪ {avito_imv/yandex_valuation/
  cian_valuation}. Детерминированно отсортирован.
- source_counts на POST теперь тоже из analogs_lots (не полной metadata-выборки)
  → инвариант source_counts.keys() ⊆ sources_used на POST и GET.
- POST персистит канонический sources_used в колонку (history/PDF консистентны для
  новых строк); GET рехайдрейтит его же helper'ом — чинит и СТАРЫЕ строки
  (листинговая часть пересобирается из analogs, quarter_index/радиусный шум
  отбрасывается фильтром оценочных флагов).

Оценочные флаги персистятся в колонке sources_used и читаются оттуда на GET —
реконструкция не требуется.

Repro 451de30b: до — sources_used=[avito,avito_imv,domklik,yandex], source_counts
имеет cian (не в sources_used); после — sources_used=[avito,avito_imv,cian,
domklik,yandex] (5/7), counts.keys() ⊆ sources_used.

Part of #2087 (M1).
lekss361 merged commit 227b939778 into main 2026-07-02 14:31:16 +00:00
lekss361 deleted branch fix/tradein-v2-source-counter-canonical 2026-07-02 14:31:16 +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#2144
No description provided.