diff --git a/tradein-mvp/backend/app/services/estimator.py b/tradein-mvp/backend/app/services/estimator.py index 7476c21d..c5387111 100644 --- a/tradein-mvp/backend/app/services/estimator.py +++ b/tradein-mvp/backend/app/services/estimator.py @@ -1818,6 +1818,14 @@ async def estimate_quality( f"Оценка построена по {anchor['n']} аналогам из {tier_label}" f"{' (топ-уровень в доме)' if anchor['used_uplift'] else ''}." ) + repair_note + # #695 (QA fixup): когда якорь подменяет headline и список аналогов на + # комплы дома, n_analogs ДОЛЖЕН считаться по anchor-популяции, а не по + # радиусу (listings_clean). Иначе все UI-счётчики («Аналогов N», + # «Показано N из M», «по N аналогам», гистограмма) расходятся: radius=4 vs + # anchor=5 → артефакт «Показано 5 из 4». anchor['n'] = число комплов, на + # которых построена оценка (= показываемый analogs, capped 10). Для anchor- + # пути это и есть «полное число найденных» по контракту n_analogs (#698). + n_analogs = anchor["n"] # ── #651: IMV / Yandex BLEND (killer accuracy fix) — SECONDARY, Tier D only ── # Радиусная медиана системно занижает премиум/видовые квартиры (нет class/ diff --git a/tradein-mvp/backend/tests/test_same_building_anchor.py b/tradein-mvp/backend/tests/test_same_building_anchor.py index 6d55e99b..b555b546 100644 --- a/tradein-mvp/backend/tests/test_same_building_anchor.py +++ b/tradein-mvp/backend/tests/test_same_building_anchor.py @@ -947,3 +947,23 @@ def test_explanation_radius_path_keeps_base_text() -> None: exp = est.confidence_explanation or "" assert "Найдено" in exp assert "Оценка построена по" not in exp # якорной фразы нет на чистом радиусе + + +# ── #695 QA fixup: n_analogs синхронизируется с anchor-популяцией (radius≠anchor) ── + + +def test_anchor_n_analogs_syncs_to_anchor_count() -> None: + """#695 QA-fail: anchor подменяет headline+список на комплы дома, но n_analogs + раньше оставался радиусным → «Показано 5 из 4». radius=5 ≠ anchor=3 намеренно; + n_analogs ДОЛЖЕН стать 3 (anchor), согласуясь с «по 3 аналогам» в explanation.""" + radius = [_make_listing(price_per_m2=200_000.0) for _ in range(5)] + est = _run_estimate(anchor_comps=_SB_COMPS_PREMIUM, anchor_tier="A", radius_analogs=radius) + assert est.n_analogs == 3 # = len(_SB_COMPS_PREMIUM), НЕ 5 (радиус) + assert "по 3 аналогам" in (est.confidence_explanation or "") + + +def test_radius_path_n_analogs_unchanged() -> None: + """#695 контроль: без якоря n_analogs = радиусный счёт (поведение не меняется).""" + radius = [_make_listing(price_per_m2=200_000.0) for _ in range(5)] + est = _run_estimate(anchor_comps=[], anchor_tier=None, radius_analogs=radius) + assert est.n_analogs == 5