[tradein][v2] Живой аудит честности чисел на проде — resolved ли жалоба оценщика + остаток #2087 (эпик #2262) #2265

Closed
opened 2026-07-03 06:29:55 +00:00 by lekss361 · 4 comments
Owner

Под-issue эпика #2262. Дименсия: честность чисел / data-contract.

Задача

На ПРОДЕ (пост A1 #2152 / 1.3 #2159 / cache-poison #2175 / dedup #2173) пройти реальную оценку и проверить, что жалоба оценщика реально закрыта и числа непротиворечивы.

Чек-лист

  1. Бейдж честен: «−N% к объявлению» = round((1 − expected_sold/median)·100) и совпадает с показанными цифрами (был −23% враньё из-за hedonic #2002 → stored ratio пересчитан как дескриптор + backfill 207). Взять 5–10 живых ?id=, сверить badge vs (expected_sold, median).
  2. «ОЖИДАЕМАЯ СДЕЛКА» не гаснет транзиентно (#2175): проверить, что пустой блок = только легит n_analogs=0, не cache-poison.
  3. Диапазоны подписаны «рыночный разброс, не погрешность»; estimate_fsd_k=1.65 НЕ трогать (A2-вердикт: сужать нельзя, 80%-SLA в expected_sold PI, не в fsd_k-коридоре).
  4. Дедуп ON (#2173): n_analogs = уникальные физлоты (не раздут кросс-постингом avito/cian/domklik с идентичной ценой); инвариант #2087 M1 source_counts.keys() ⊆ sources_used держится.
  5. Остаток #2087: M2 (ratio ↔ медианы: показанный ratio согласован с median/expected на витрине), M3 (базы торга bargain — какие цены реально показываем как «сделки»). Довести или явно закрыть.

Доступ

Прод HTTP только ssh gendesign → docker exec tradein-backend curl localhost:8000 (admin -H "X-Authenticated-User: admin"). БД: mcp__postgres-tradein__* (db=tradein). Тест-кейс: Белинского 86, 54 м², 2-к.

DoD

Пройдено 5–10 живых оценок; расхождения (если есть) → PR или вывод «жалоба закрыта». #2087 M2/M3 либо done, либо явный вердикт.

Под-issue эпика #2262. **Дименсия: честность чисел / data-contract.** ## Задача На ПРОДЕ (пост A1 #2152 / 1.3 #2159 / cache-poison #2175 / dedup #2173) пройти реальную оценку и проверить, что жалоба оценщика реально закрыта и числа непротиворечивы. ## Чек-лист 1. **Бейдж честен:** «−N% к объявлению» = `round((1 − expected_sold/median)·100)` и совпадает с показанными цифрами (был −23% враньё из-за hedonic #2002 → stored ratio пересчитан как дескриптор + backfill 207). Взять 5–10 живых `?id=`, сверить badge vs (expected_sold, median). 2. **«ОЖИДАЕМАЯ СДЕЛКА» не гаснет** транзиентно (#2175): проверить, что пустой блок = только легит `n_analogs=0`, не cache-poison. 3. **Диапазоны** подписаны «рыночный разброс, не погрешность»; `estimate_fsd_k=1.65` НЕ трогать (A2-вердикт: сужать нельзя, 80%-SLA в expected_sold PI, не в fsd_k-коридоре). 4. **Дедуп ON (#2173):** `n_analogs` = уникальные физлоты (не раздут кросс-постингом avito/cian/domklik с идентичной ценой); инвариант #2087 M1 `source_counts.keys() ⊆ sources_used` держится. 5. **Остаток #2087:** M2 (ratio ↔ медианы: показанный ratio согласован с median/expected на витрине), M3 (базы торга bargain — какие цены реально показываем как «сделки»). Довести или явно закрыть. ## Доступ Прод HTTP только `ssh gendesign → docker exec tradein-backend curl localhost:8000` (admin `-H "X-Authenticated-User: admin"`). БД: `mcp__postgres-tradein__*` (db=tradein). Тест-кейс: Белинского 86, 54 м², 2-к. ## DoD Пройдено 5–10 живых оценок; расхождения (если есть) → PR или вывод «жалоба закрыта». #2087 M2/M3 либо done, либо явный вердикт.
lekss361 added the
scope/backend
tradein
labels 2026-07-03 06:31:15 +00:00
Author
Owner

Живой аудит выполнен (прод, 2026-07-03)

Вердикт: жалоба оценщика закрыта. 657/657 оценок (all-time): stored asking_to_sold_ratio == expected_sold_price/median_price, max δ = 0.0000. Live-кейс Белинского 86 (2к 54м², id 41937b72): median 13 793 279 / expected 11 308 359 / ratio 0.8198 → бейдж 18% сходится по обеим формулам (HeroSummary.tsx:118-124). Таблица 10 живых оценок — все OK.

Чек-лист:

  1. Бейдж честен (10/10 живых + 657/657 stored).
  2. ⚠️ «ОЖИДАЕМАЯ СДЕЛКА» пуста: 6/9 легитимных n_analogs=0; 3/224 (1.3%) — изолированный transient DB-error (не cache-poison, #2175 держится: соседние оценки в том же бакете получили ratio). MINOR-фикс: savepoint+retry вокруг ratio-lookup → в PR.
  3. ⚠️ SERIOUS: дедуп включён, но протекает. Один физлот (Сыромолотова, 4.0 млн, 33.6 м², эт. 3) = 3 «аналога» через cian/domklik/yandex: cadnum пуст (domklik — 0/6296 всегда!), а _norm_addr_for_dedup не схлопывает source-форматы адреса. n_analogs/source_counts раздуты. Инвариант M1 source_counts.keys() ⊆ sources_used при этом цел. Фикс (устойчивый street+house ключ + композит-fallback) → PR.
  4. M2 DONE: ratio ↔ медианы ↔ expected взаимосогласованы на витрине.
  5. M3 DONE: «сделки» = реальные ДКП Росреестра (StreetDealsCard/DealsCard), ДДУ вырезаны (#549); снятые листинги честно подписаны «Недавно снятые», не «продано». PR не нужен.

PR на п.2+п.3 — следующим комментом.

## Живой аудит выполнен (прод, 2026-07-03) **Вердикт: жалоба оценщика закрыта.** 657/657 оценок (all-time): stored `asking_to_sold_ratio` == `expected_sold_price/median_price`, max δ = 0.0000. Live-кейс Белинского 86 (2к 54м², id 41937b72): median 13 793 279 / expected 11 308 359 / ratio 0.8198 → бейдж 18% сходится по обеим формулам (`HeroSummary.tsx:118-124`). Таблица 10 живых оценок — все OK. **Чек-лист:** 1. ✅ Бейдж честен (10/10 живых + 657/657 stored). 2. ✅→⚠️ «ОЖИДАЕМАЯ СДЕЛКА» пуста: 6/9 легитимных `n_analogs=0`; **3/224 (1.3%) — изолированный transient DB-error** (не cache-poison, #2175 держится: соседние оценки в том же бакете получили ratio). MINOR-фикс: savepoint+retry вокруг ratio-lookup → в PR. 3. ⚠️ **SERIOUS: дедуп включён, но протекает.** Один физлот (Сыромолотова, 4.0 млн, 33.6 м², эт. 3) = 3 «аналога» через cian/domklik/yandex: cadnum пуст (domklik — 0/6296 всегда!), а `_norm_addr_for_dedup` не схлопывает source-форматы адреса. n_analogs/source_counts раздуты. Инвариант M1 `source_counts.keys() ⊆ sources_used` при этом цел. Фикс (устойчивый street+house ключ + композит-fallback) → PR. 4. ✅ **M2 DONE:** ratio ↔ медианы ↔ expected взаимосогласованы на витрине. 5. ✅ **M3 DONE:** «сделки» = реальные ДКП Росреестра (StreetDealsCard/DealsCard), ДДУ вырезаны (#549); снятые листинги честно подписаны «Недавно снятые», не «продано». PR не нужен. PR на п.2+п.3 — следующим комментом.
Author
Owner

Фиксы влиты: PR #2273 (merged 2026-07-03)

  1. Дедуп-ключ переписан (SERIOUS из аудита): нормализованный street_token + композит (street, floor, area_bucket ±0.5м², price_bucket 100k) + union-find по любому совпавшему ключу (решает cadnum-асимметрию) + два guard'а: разные непустые кадастры никогда не сливаются; разные извлечённые номера домов блокируют street-union (equal-or-missing — сливаем). Нумерованные улицы («8 Марта», «40 лет Октября») поддержаны. Прод-кейс Сыромолотова (1 физлот = 3 «аналога») закрыт тестами на всех трёх source-форматах.
  2. Savepoint+retry вокруг ratio-lookup (MINOR): transient DB-error больше не гасит expected_sold (было 3/224 за 14 дней); #2175-контракт (None не кэшируется) сохранён.

Ревью: MINOR → все замечания закрыты follow-up коммитом (нумерованные улицы + house-no guard + регрессионные тесты). CI-инцидент по пути: новая module-level константа _STREET_NAME_RE коллидировала с существующей (используется extract_street_name в street-deals эндпоинтах) → 14 тестов вне dedup-скоупа упали; переименовано с префиксом _DEDUP_. Полный сьют: 3067 passed.

Post-deploy QA: переоценка кейса Рассветная 5 (d5fc3841) — n_analogs без триплетов; новые оценки с заполненным expected_sold.

## Фиксы влиты: PR #2273 (merged 2026-07-03) 1. **Дедуп-ключ переписан** (SERIOUS из аудита): нормализованный street_token + композит (street, floor, area_bucket ±0.5м², price_bucket 100k) + union-find по любому совпавшему ключу (решает cadnum-асимметрию) + два guard'а: разные непустые кадастры никогда не сливаются; разные извлечённые номера домов блокируют street-union (equal-or-missing — сливаем). Нумерованные улицы («8 Марта», «40 лет Октября») поддержаны. Прод-кейс Сыромолотова (1 физлот = 3 «аналога») закрыт тестами на всех трёх source-форматах. 2. **Savepoint+retry вокруг ratio-lookup** (MINOR): transient DB-error больше не гасит expected_sold (было 3/224 за 14 дней); #2175-контракт (None не кэшируется) сохранён. Ревью: MINOR → все замечания закрыты follow-up коммитом (нумерованные улицы + house-no guard + регрессионные тесты). CI-инцидент по пути: новая module-level константа `_STREET_NAME_RE` коллидировала с существующей (используется `extract_street_name` в street-deals эндпоинтах) → 14 тестов вне dedup-скоупа упали; переименовано с префиксом `_DEDUP_`. Полный сьют: **3067 passed**. Post-deploy QA: переоценка кейса Рассветная 5 (d5fc3841) — n_analogs без триплетов; новые оценки с заполненным expected_sold.
Author
Owner

QA-smoke на проде (2026-07-03) — FAIL, gap найден в PR #2273

Живой запрос POST /api/v1/trade-in/estimate, адрес «Рассветная 5», 33 м², 1-к (после деплоя PR #2273) воспроизводит ТОТ ЖЕ класс бага, что фиксился в этом issue:

Физлот «ул. Сыромолотова, 11», area_m2=33.6, floor=3, price_rub=4 000 000 — встречается дважды в analogs[] одной оценки (estimate_id=3b734da3-...): source=domklik (listings.id=421317, cadastral_number пуст, house_id_fk=192241) и source=cian (listings.id=103596, cadastral_number пуст, house_id_fk=192241) — идентичные площадь/этаж/цена.

Root cause — gap в покрытии, не в алгоритме. _dedup_cross_source() (введена PR #2273) вызывается ровно в одном месте — внутри _price_from_inputs(), только для радиусного списка listings. Прямой вызов этой функции в живом контейнере на найденной паре корректно схлопывает её в 1 запись — сам union-find работает. Но _fetch_anchor_comps() (строит Tier A same_building / Tier C micro_radius через отдельный raw SQL) никогда не проходит через дедуп, а estimate_quality() берёт показываемые analogs[] из недедупленного anchor_comps_used, а не из listings_clean, как только сработал якорный тир. Именно якорный путь (analog_tier=micro_radius) сработал на тестовом адресе — 9 тестов PR #2273 покрывают только радиусный путь, поэтому баг прошёл незамеченным.

Побочный эффект: счётчик n_analogs считается из радиусной (задедупленной) выборки, а показанные top-N analogs — из другой (anchor, недедуп) — счётчик не описывает то, что реально отображается.

expected_sold_price_rub NOT NULL и source_counts ⊆ sources_used — в порядке, savepoint-часть фикса работает.

Вердикт: FAIL. Issue остаётся open, эпик #2262 не отмечаю по этому пункту. Follow-up фикс (дедуп на anchor-пути + синхронизация счётчика + regression-тест именно на этот сценарий) уже в работе, отчитаюсь отдельно после мерджа.

## QA-smoke на проде (2026-07-03) — FAIL, gap найден в PR #2273 Живой запрос POST /api/v1/trade-in/estimate, адрес «Рассветная 5», 33 м², 1-к (после деплоя PR #2273) воспроизводит ТОТ ЖЕ класс бага, что фиксился в этом issue: Физлот «ул. Сыромолотова, 11», area_m2=33.6, floor=3, price_rub=4 000 000 — встречается **дважды** в `analogs[]` одной оценки (estimate_id=3b734da3-...): `source=domklik` (listings.id=421317, cadastral_number пуст, house_id_fk=192241) и `source=cian` (listings.id=103596, cadastral_number пуст, house_id_fk=192241) — идентичные площадь/этаж/цена. **Root cause — gap в покрытии, не в алгоритме.** `_dedup_cross_source()` (введена PR #2273) вызывается ровно в одном месте — внутри `_price_from_inputs()`, только для радиусного списка `listings`. Прямой вызов этой функции в живом контейнере на найденной паре корректно схлопывает её в 1 запись — сам union-find работает. Но `_fetch_anchor_comps()` (строит Tier A same_building / Tier C micro_radius через отдельный raw SQL) никогда не проходит через дедуп, а `estimate_quality()` берёт показываемые `analogs[]` из недедупленного `anchor_comps_used`, а не из `listings_clean`, как только сработал якорный тир. Именно якорный путь (`analog_tier=micro_radius`) сработал на тестовом адресе — 9 тестов PR #2273 покрывают только радиусный путь, поэтому баг прошёл незамеченным. Побочный эффект: счётчик `n_analogs` считается из радиусной (задедупленной) выборки, а показанные top-N analogs — из другой (anchor, недедуп) — счётчик не описывает то, что реально отображается. `expected_sold_price_rub` NOT NULL и `source_counts ⊆ sources_used` — в порядке, savepoint-часть фикса работает. **Вердикт: FAIL. Issue остаётся open, эпик #2262 не отмечаю по этому пункту.** Follow-up фикс (дедуп на anchor-пути + синхронизация счётчика + regression-тест именно на этот сценарий) уже в работе, отчитаюсь отдельно после мерджа.
Author
Owner

Follow-up PR #2311 — post-merge QA PASS

Задеплоено ~20:39 UTC. Живой repro на том же кейсе («Рассветная 5», 33м², 1-к) — теперь маршрутизируется через analog_tier=micro_radius (anchor-путь, ранее баговый). Физлот «ул. Сыромолотова, 11» встречается ровно один раз. n_analogs=18 честно соответствует количеству показанных analogs.

Смоукинг-ган из логов бэкенда на этом же запросе: сработали ДВЕ независимые дедуп-волны — радиусный пул 40→28 (-12 dups) и новый анкорный пул 27→18 (-9 dups). Пост-дедуп анкорный count (18) точно совпадает с n_analogs в ответе — подтверждает, что фикс реально в проде и его вывод фидит финальную статистику.

Регрессии нет: /health 200, GlitchTip чист после деплоя (новых issues с firstSeen после 20:35 нет).

Issue закрыт. Эпик #2262 отмечаю по этому пункту.

## Follow-up PR #2311 — post-merge QA PASS Задеплоено ~20:39 UTC. Живой repro на том же кейсе («Рассветная 5», 33м², 1-к) — теперь маршрутизируется через `analog_tier=micro_radius` (anchor-путь, ранее баговый). Физлот «ул. Сыромолотова, 11» встречается **ровно один раз**. n_analogs=18 честно соответствует количеству показанных analogs. Смоукинг-ган из логов бэкенда на этом же запросе: сработали ДВЕ независимые дедуп-волны — радиусный пул `40→28 (-12 dups)` и **новый** анкорный пул `27→18 (-9 dups)`. Пост-дедуп анкорный count (18) точно совпадает с `n_analogs` в ответе — подтверждает, что фикс реально в проде и его вывод фидит финальную статистику. Регрессии нет: `/health` 200, GlitchTip чист после деплоя (новых issues с firstSeen после 20:35 нет). **Issue закрыт.** Эпик #2262 отмечаю по этому пункту.
Sign in to join this conversation.
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#2265
No description provided.