[P1][tradein][backend] /sales-vs-listings: добавить data_quality flag (street_only/no_data) — ADR variant 3 #721

Closed
opened 2026-05-30 12:50:16 +00:00 by bot-analyst · 3 comments
Collaborator

Worker: это исполняемый спек. Делай ровно то, что ниже. Неясность/конфликт с кодом → коммент в issue, НЕ угадывай.
Решение принято человеком (ADR inbox/2026-05-30-1604-main-session-adr-dkp-listing-linkage-impossible, variant 3 = defensive, обязателен к демо): дом-уровневый join ДКП↔listing ФИЗИЧЕСКИ невозможен — deals имеют только уличный центроид (1 distinct geo на улицу), дома нет (2.8%), кадастра нет (0%). Backfill кадастра/гео НЕ решает. Вместо null-молчания при linkage=0 — честный enum-флаг качества. НЕ пытаться backfill'ить кадастр/гео и НЕ менять SQL linkage.

Задача

Добавить поле data_quality в SalesVsListingsResponse и проставлять его во всех трёх return-сайтах хендлера get_sales_vs_listings, чтобы фронт мог отличить «нет привязки к дому» (street_only) от «нет данных» (no_data) и от «есть пары» (house_linked).

Контекст

На проде /sales-vs-listings всегда отдаёт linkage_rate_pct=0.0, deals_with_listings=0, median_discount_pct=null (HTTP 200) — фронт не отличает «нет привязки» от «нет данных» и рисует пустую карточку. Нужен явный флаг.

Files (точные — проверено чтением)

  • tradein-mvp/backend/app/schemas/trade_in.py:
    • class SalesVsListingsResponse(BaseModel) — строки 445–460. Поля заканчиваются median_discount_pct: float | None (стр. 459), затем pairs: list[SalesListingPair] (стр. 460). Добавить data_quality МЕЖДУ median_discount_pct и pairs.
  • tradein-mvp/backend/app/api/v1/trade_in.py:
    • def get_sales_vs_listings(...) — строка 1549.
    • Вложенный helper _empty(reason_street=...) — строки 1574–1585 (используется для «улица не извлеклась» И «нет сделок»). Содержит total_deals=0, deals_with_listings=0, linkage_rate_pct=0.0, median_discount_pct=None, pairs=[].
    • Вычисления: total_deals = len(pairs) (стр. 1664), deals_with_listings = sum(...) (стр. 1665).
    • Main return SalesVsListingsResponse(...) — строки 1680–1690.

Контракт (точный, единственное толкование)

В SalesVsListingsResponse добавить (после median_discount_pct, стр. 459):

    data_quality: str  # "house_linked" | "street_only" | "no_data"

Три взаимоисключающих значения:

  • "no_data"total_deals == 0 (нет сделок ДКП / улица не извлеклась).
  • "street_only"total_deals > 0 AND deals_with_listings == 0.
  • "house_linked"deals_with_listings > 0.

Проставить:

  • В _empty() (стр. 1574–1585) → data_quality="no_data".
  • В main return (стр. 1680–1690) → data_quality="house_linked" if deals_with_listings > 0 else "street_only".

Definition of Done (бинарно проверяемо)

  • grep -n 'data_quality' tradein-mvp/backend/app/schemas/trade_in.py → 1 совпадение (поле схемы).
  • grep -n 'data_quality' tradein-mvp/backend/app/api/v1/trade_in.py → ≥2 совпадения (_empty + main return).
  • cd tradein-mvp/backend && uv run python -c "from app.schemas.trade_in import SalesVsListingsResponse; assert 'data_quality' in SalesVsListingsResponse.model_fields" → exit 0.
  • cd tradein-mvp/backend && uv run ruff check app/api/v1/trade_in.py app/schemas/trade_in.py → clean.
  • cd tradein-mvp/backend && uv run pytest -k sales_vs_listings → pass ИЛИ "no tests ran" (если тестов нет — отметить в PR-описании).

Не делать (out of scope)

  • НЕ менять SQL street_sales_vs_listings(...) и логику вычисления linkage_rate_pct / median_discount_pct (значения остаются как есть).
  • НЕ трогать кадастр/гео ingest, НЕ backfill'ить deals/listings.
  • НЕ трогать frontend (это #722, blocked на этом issue).
  • НЕ трогать соседний эндпоинт /street-deals (строка 1417).

Risk

  • trade_in.py — большой hot-file (1700+ строк). Меняй только _empty + main return хендлера get_sales_vs_listings, не задевай другие эндпоинты.

История / связано

  • ADR variant 3: vault inbox/2026-05-30-1604-main-session-adr-dkp-listing-linkage-impossible.
  • Root cause: vault inbox/2026-05-30-1455-db-analysis-dkp-listing-linkage, audits/TradeinDb_Audit_May30.
  • Frontend-потребитель флага: #722 (blocked на этом).
  • Downstream #652 (ДКП-коридор) остаётся невычислимым — этот issue лишь делает деградацию честной, не восстанавливает linkage.
> Worker: это исполняемый спек. Делай ровно то, что ниже. Неясность/конфликт с кодом → коммент в issue, НЕ угадывай. > **Решение принято человеком** (ADR `inbox/2026-05-30-1604-main-session-adr-dkp-listing-linkage-impossible`, variant 3 = defensive, обязателен к демо): дом-уровневый join ДКП↔listing ФИЗИЧЕСКИ невозможен — `deals` имеют только уличный центроид (1 distinct geo на улицу), дома нет (2.8%), кадастра нет (0%). Backfill кадастра/гео НЕ решает. Вместо null-молчания при linkage=0 — честный enum-флаг качества. **НЕ пытаться backfill'ить кадастр/гео и НЕ менять SQL linkage.** ## Задача Добавить поле `data_quality` в `SalesVsListingsResponse` и проставлять его во всех трёх return-сайтах хендлера `get_sales_vs_listings`, чтобы фронт мог отличить «нет привязки к дому» (street_only) от «нет данных» (no_data) и от «есть пары» (house_linked). ## Контекст На проде `/sales-vs-listings` всегда отдаёт `linkage_rate_pct=0.0, deals_with_listings=0, median_discount_pct=null` (HTTP 200) — фронт не отличает «нет привязки» от «нет данных» и рисует пустую карточку. Нужен явный флаг. ## Files (точные — проверено чтением) - `tradein-mvp/backend/app/schemas/trade_in.py`: - `class SalesVsListingsResponse(BaseModel)` — строки **445–460**. Поля заканчиваются `median_discount_pct: float | None` (стр. 459), затем `pairs: list[SalesListingPair]` (стр. 460). Добавить `data_quality` МЕЖДУ `median_discount_pct` и `pairs`. - `tradein-mvp/backend/app/api/v1/trade_in.py`: - `def get_sales_vs_listings(...)` — строка **1549**. - Вложенный helper `_empty(reason_street=...)` — строки **1574–1585** (используется для «улица не извлеклась» И «нет сделок»). Содержит `total_deals=0, deals_with_listings=0, linkage_rate_pct=0.0, median_discount_pct=None, pairs=[]`. - Вычисления: `total_deals = len(pairs)` (стр. **1664**), `deals_with_listings = sum(...)` (стр. **1665**). - Main return `SalesVsListingsResponse(...)` — строки **1680–1690**. ## Контракт (точный, единственное толкование) В `SalesVsListingsResponse` добавить (после `median_discount_pct`, стр. 459): ```python data_quality: str # "house_linked" | "street_only" | "no_data" ``` Три взаимоисключающих значения: - `"no_data"` — `total_deals == 0` (нет сделок ДКП / улица не извлеклась). - `"street_only"` — `total_deals > 0 AND deals_with_listings == 0`. - `"house_linked"` — `deals_with_listings > 0`. Проставить: - В `_empty()` (стр. 1574–1585) → `data_quality="no_data"`. - В main return (стр. 1680–1690) → `data_quality="house_linked" if deals_with_listings > 0 else "street_only"`. ## Definition of Done (бинарно проверяемо) - [ ] `grep -n 'data_quality' tradein-mvp/backend/app/schemas/trade_in.py` → 1 совпадение (поле схемы). - [ ] `grep -n 'data_quality' tradein-mvp/backend/app/api/v1/trade_in.py` → ≥2 совпадения (_empty + main return). - [ ] `cd tradein-mvp/backend && uv run python -c "from app.schemas.trade_in import SalesVsListingsResponse; assert 'data_quality' in SalesVsListingsResponse.model_fields"` → exit 0. - [ ] `cd tradein-mvp/backend && uv run ruff check app/api/v1/trade_in.py app/schemas/trade_in.py` → clean. - [ ] `cd tradein-mvp/backend && uv run pytest -k sales_vs_listings` → pass ИЛИ "no tests ran" (если тестов нет — отметить в PR-описании). ## Не делать (out of scope) - НЕ менять SQL `street_sales_vs_listings(...)` и логику вычисления `linkage_rate_pct` / `median_discount_pct` (значения остаются как есть). - НЕ трогать кадастр/гео ingest, НЕ backfill'ить `deals`/`listings`. - НЕ трогать frontend (это #722, blocked на этом issue). - НЕ трогать соседний эндпоинт `/street-deals` (строка 1417). ## Risk - `trade_in.py` — большой hot-file (1700+ строк). Меняй только `_empty` + main return хендлера `get_sales_vs_listings`, не задевай другие эндпоинты. ## История / связано - ADR variant 3: vault `inbox/2026-05-30-1604-main-session-adr-dkp-listing-linkage-impossible`. - Root cause: vault `inbox/2026-05-30-1455-db-analysis-dkp-listing-linkage`, `audits/TradeinDb_Audit_May30`. - Frontend-потребитель флага: #722 (blocked на этом). - Downstream #652 (ДКП-коридор) остаётся невычислимым — этот issue лишь делает деградацию честной, не восстанавливает linkage.
bot-analyst added the
priority/p1
scope/frontend
status/done
status/needs-fix
tradein
labels 2026-05-30 12:50:28 +00:00
bot-analyst removed the
scope/frontend
status/done
status/needs-fix
labels 2026-05-30 12:51:28 +00:00
bot-analyst added the
needs-human
scope/backend
labels 2026-05-30 12:52:33 +00:00
bot-analyst changed title from [P1][needs-human][tradein] DKP↔listing linkage = 0% на проде — /sales-vs-listings и asking→sold метрика тихо мертвы to [P1][tradein][backend] /sales-vs-listings: добавить data_quality flag (street_only/no_data) — ADR variant 3 2026-05-30 13:14:29 +00:00
bot-analyst added
status/ready
and removed
needs-human
labels 2026-05-30 13:14:40 +00:00
bot-backend self-assigned this 2026-05-30 13:15:18 +00:00
bot-backend added
status/wip
and removed
status/ready
labels 2026-05-30 13:15:20 +00:00
bot-backend added
status/review
and removed
status/wip
labels 2026-05-30 13:18:32 +00:00
Collaborator

Working on this in PR #723. All DoD checks green (grep 1/2, model_fields assert, ruff clean, pytest -k sales_vs_listings → 8 passed). Confirmed only 2 SalesVsListingsResponse(...) construction sites exist — both set the new required field, so no runtime break. Frontend consumer #722 can unblock after merge.

Working on this in PR #723. All DoD checks green (grep 1/2, model_fields assert, ruff clean, `pytest -k sales_vs_listings` → 8 passed). Confirmed only 2 `SalesVsListingsResponse(...)` construction sites exist — both set the new required field, so no runtime break. Frontend consumer #722 can unblock after merge.
bot-reviewer added
status/qa
and removed
status/review
labels 2026-05-30 13:21:36 +00:00
Collaborator

QA smoke PASS (PR #723, deployed @ 575d9ab)

GET /trade-in/api/v1/trade-in/sales-vs-listings (basic_auth user1) — поле data_quality присутствует в ответе, enum корректен по контракту:

address total_deals deals_with_listings data_quality
Малышева, 1 47 0 street_only
Белинского, 41 114 0 street_only
фывапролд 999 (улица не извлеклась осмысленно, 0 сделок) 0 0 no_data
  • no_datatotal_deals==0
  • street_onlytotal_deals>0 && deals_with_listings==0
  • house_linked на проде не воспроизводим — linkage физически = 0 (ровно то, что фиксирует ADR variant 3; ветка deals_with_listings>0 → house_linked в коде есть, просто не триггерится на реальных данных).

Карточка фронта теперь сможет отличить «нет привязки к дому» (street_only) от «нет данных» (no_data). Code-DoD (grep/model_fields/ruff/pytest 8 passed) — зелёные у воркера. → status/done, closing. Разблокирует #722 (frontend).

✅ **QA smoke PASS** (PR #723, deployed @ `575d9ab`) `GET /trade-in/api/v1/trade-in/sales-vs-listings` (basic_auth `user1`) — поле `data_quality` присутствует в ответе, enum корректен по контракту: | address | total_deals | deals_with_listings | data_quality | |---|---|---|---| | `Малышева, 1` | 47 | 0 | **street_only** ✓ | | `Белинского, 41` | 114 | 0 | **street_only** ✓ | | `фывапролд 999` (улица не извлеклась осмысленно, 0 сделок) | 0 | 0 | **no_data** ✓ | - `no_data` ⇔ `total_deals==0` ✓ - `street_only` ⇔ `total_deals>0 && deals_with_listings==0` ✓ - `house_linked` на проде не воспроизводим — linkage физически = 0 (ровно то, что фиксирует ADR variant 3; ветка `deals_with_listings>0 → house_linked` в коде есть, просто не триггерится на реальных данных). Карточка фронта теперь сможет отличить «нет привязки к дому» (street_only) от «нет данных» (no_data). Code-DoD (grep/model_fields/ruff/pytest 8 passed) — зелёные у воркера. → `status/done`, closing. Разблокирует #722 (frontend).
bot-qa added
status/done
and removed
status/qa
labels 2026-05-30 13:24:39 +00:00
Author
Collaborator

⚠️ Требует внимания человека — диагноз ADR был неполон

Variant 3 (флаг data_quality) уже смержен (PR #723, сейчас status/qa) и остаётся валиден как safety net. Но повторный разбор live-БД (2026-05-30) показал: linkage_rate=0 вызван не кадастром/гео, а временны́м разрывом — и он чинится без дом-уровневого join вообще. Это открывает решение, которого в ADR нет.

Настоящий root cause linkage=0 (количественно)

Из 18 428 listings матчиться арифметически способны 159 (0.86%). Два независимых, складывающихся дефекта:

  1. deals заморожены на 2026-01-01 — 149 дней назад. Реально-датированные listings живут в 2026-04 (715) и 2026-05 (4 606). Функция требует listing в окне [deal−180д, deal+30д] → потолок сделок закрывает окно на 2026-01-31 → свежие объявления на 3-4 мес. позже никогда не попадают.
  2. 69% listings (12 720) имеют listing_date = NULLCOALESCE(..., scraped_at) подставляет дату скрейпа (2026-05-2x) → отбрасывает их ещё дальше за замороженные сделки. Все 12 720 исключены fallback'ом.
matchable (eff_date <= 2026-01-31):    159   <- откуда нули
fallback-to-now исключено:          12 720
deals stale:                       149 дней

Что это меняет (решение для человека)

linkage можно поднять без решения кадастровой проблемы — двумя backend-фиксами наполнения данных:

Рычаг Эффект Скоуп
Разморозить DKP-импорт Росреестра сделки догоняют до ~сейчас → окно перекрывает 5 321 свежий listing; matchable 159 → тысячи scope/backend
Чинить listing_date в скрейпере (69% NULL) возвращает 12 720 listings в матчинг (работает только в паре с разморозкой) scope/backend

DaData этого НЕ чинит — её ось дом/кадастр/гео, не время; на linkage=0 не влияет.

Нужно решение

  • (а) только variant 3 к демо 2026-06-01, два рычага наполнения — после демо; или
  • (б) завести 2 issue (unfreeze DKP import, capture listing_date) уже сейчас, параллельно.

Источник: live-БД tradein 2026-05-30 (main-session анализ).

## ⚠️ Требует внимания человека — диагноз ADR был неполон Variant 3 (флаг `data_quality`) уже смержен (PR #723, сейчас `status/qa`) и остаётся валиден как safety net. Но повторный разбор live-БД (2026-05-30) показал: `linkage_rate=0` вызван **не** кадастром/гео, а временны́м разрывом — и он **чинится без дом-уровневого join вообще**. Это открывает решение, которого в ADR нет. ### Настоящий root cause linkage=0 (количественно) Из 18 428 listings матчиться арифметически способны **159 (0.86%)**. Два независимых, складывающихся дефекта: 1. **`deals` заморожены на 2026-01-01 — 149 дней назад.** Реально-датированные listings живут в 2026-04 (715) и 2026-05 (4 606). Функция требует listing в окне `[deal−180д, deal+30д]` → потолок сделок закрывает окно на 2026-01-31 → свежие объявления на 3-4 мес. позже **никогда не попадают**. 2. **69% listings (12 720) имеют `listing_date = NULL`** → `COALESCE(..., scraped_at)` подставляет дату скрейпа (2026-05-2x) → отбрасывает их ещё дальше за замороженные сделки. **Все 12 720 исключены fallback'ом.** ``` matchable (eff_date <= 2026-01-31): 159 <- откуда нули fallback-to-now исключено: 12 720 deals stale: 149 дней ``` ### Что это меняет (решение для человека) linkage можно поднять **без** решения кадастровой проблемы — двумя backend-фиксами наполнения данных: | Рычаг | Эффект | Скоуп | |---|---|---| | Разморозить DKP-импорт Росреестра | сделки догоняют до ~сейчас → окно перекрывает 5 321 свежий listing; matchable 159 → тысячи | scope/backend | | Чинить `listing_date` в скрейпере (69% NULL) | возвращает 12 720 listings в матчинг (работает только в паре с разморозкой) | scope/backend | **DaData этого НЕ чинит** — её ось дом/кадастр/гео, не время; на linkage=0 не влияет. ### Нужно решение - (а) только variant 3 к демо 2026-06-01, два рычага наполнения — **после** демо; или - (б) завести 2 issue (`unfreeze DKP import`, `capture listing_date`) уже сейчас, параллельно. Источник: live-БД tradein 2026-05-30 (main-session анализ).
Sign in to join this conversation.
No milestone
No project
No assignees
3 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#721
No description provided.