Review-bot findings:
- CRITICAL: expected_sold_* were derived from PRE-blend median/range/ppm2, so
after the #651 blend raised median_price the UI showed asking (blended) vs
expected-sold (un-blended) → nonsensical discount. Moved the asking→sold
derivation to AFTER the blend block; INSERT + returned estimate now use the
post-blend values.
- IMV anchor had no rooms/area band → a studio-only IMV row could anchor a
3-room target and inflate via the x1.15 trigger. Added NULL-tolerant guard
(abs(Δrooms)<=1 AND area_m2 BETWEEN area*0.7..1.3) in _fetch_house_imv_anchor.
- Tests: expected_sold↔blended-median consistency, band rejection, weight clamp
[0,1], corridor count branch. ruff clean; 26 + 145 estimator regression pass.
Frontend surfacing in the DEPLOYED trade-in UI (tradein-mvp/frontend):
- types/trade-in.ts: add AvitoImvSummary + DkpCorridor, optional avito_imv /
dkp_corridor on AggregatedEstimate (mirror backend snake_case, no any)
- HeroSummary.tsx: 'Avito IMV: X млн ₽' caption near price + 'Коридор реальных
сделок: X–Y млн · N ДКП (Росреестр)' line. No new fetch, no-op when null.
tsc --noEmit clean. Root legacy frontend/ untouched.