- новый src/lib/source-registry.ts — один источник правды (SOURCES as const,
канон сверен с estimator._canonical_sources); n1 live:false, убран из ростера
- HeroSummary: убран сфабрикованный «/7»; пустой sources_used → «источники не
зафиксированы» вместо фейк-чипов; убрана выдуманная «классическая»; CV →
честная метрика (реальный estimate.cv или лейбл «разброс»)
- ListingsCard: знаменатель убран совсем (sources_used включает оценочные
источники — фиксированный знаменатель либо врёт, либо меньше числителя);
fix невидимой rosreestr-точки (dot "ros"→"rosreestr", класса .ros нет в CSS)
- SourcesProgress: ростер из registry, мёртвый N1 убран
- EstimateForm: убран всегда-зелёный фейковый «● готов» cache-индикатор
- page.tsx: восстановленный отчёт показывает created_at, не new Date()
- types: +cv/source_counts/created_at (уже были в backend-схеме)
- инвариант: type-level (as const satisfies) + контракт-комментарий против
хардкод-массивов источников
Refs #2211
Оценщик жаловался на «большой интервал» между рекомендованной ценой и
оценкой. Бэктест: ширину коридора косметически не сузить (реальная рыночная
дисперсия + asking→sold gap). Чиним подачу, не число (математика не тронута).
Витрина /trade-in/v2 (активная, wired на real data через mapResultPanel):
- deltaLabel «К РЫНКУ» → «К ОБЪЯВЛ.» (mappers.ts + fixtures.ts): −N% —
разрыв «объявление → ожидаемая сделка», а не «скидка от нас».
- ResultPanel: честная подпись под ценовыми карточками — «Диапазоны
показывают разброс цен на рынке, а не погрешность оценки». Широкий
диапазон не читается как неуверенность расчёта.
- Иерархия expected_sold=headline / median=baseline / ДКП=справочно уже
была (audit H2, #2062/#2081) — не трогаем.
Легаси-витрина / (HeroSummary), тот же принцип и устранение конфликта термина:
- hero-duo: expected_sold — цифра-герой («Оценка · ожидаемая цена продажи»,
34px, accent), median — вторичный «Рекомендованная цена в объявлении»
(20px, приглушённый) вместо двух равнозначных 28px-блоков «Выставить»/
«Получить». Новый модификатор .hero-duo--sold-primary.
- Диапазон sold подписан «рыночный разброс» + honest hint «Диапазон отражает
разброс цен по рынку, а не погрешность оценки».
- Бейдж «−N%» → «−N% к объявлению» + title (разрыв asking↔сделка, не скидка).
- OfferCard: «Оценка по рынку» → «Цена в объявлении» (median ≠ «оценка»;
слово «оценка» на витрине = expected_sold).
Проверка: tsc --noEmit чисто; next build чисто (lint+types, 13 роутов).
- types/trade-in.ts: добавлен AnalogTier тип и analog_tier? поле в AggregatedEstimate (опциональное, graceful при absent/null)
- HeroTransparency: resolveCompsTier() предпочитает analog_tier (структурный, без парсинга строки), fallback на compsTier() эвристику при absent/null; добавлена строка «Точность адреса» в collapsible «Как рассчитано» из address_precision с предупреждением при low+approximate
- HeroSummary: третий бар «Ожидаемая цена сделки» (expected_sold_price_rub/low/high) с отличным цветом (viz-5/6) — graceful при null; ratio_basis показывается всегда: per_rooms→«по сделкам той же планировки», global_fallback→прежний текст
AggregatedEstimate не нёс параметры целевой квартиры (площадь, этаж,
комнаты, год, тип дома). HeroSummary брал их из формы-инпута — у
свежей оценки работало, но при открытии оценки по ссылке (?id=, из
Истории) формы нет → «Сводка» показывала «ЭТАЖ 0/0, Площадь 0 м²».
Параметры теперь проходят сквозь весь стек:
- AggregatedEstimate + TS-тип получили area_m2/rooms/floor/
total_floors/year_built/house_type/repair_state/has_balcony;
- estimate_quality заполняет их из payload, get_estimate — из строки
trade_in_estimates (колонки там уже есть);
- HeroSummary читает из estimate с откатом на input.
Заодно в TS-тип добавлен пропущенный est_days_on_market.
UI-баги, замеченные при обзоре результата оценки:
- HeroSummary: блок hero-photo был пустым серым плейсхолдером (в
макете там фото, в коде слот не подключён). Теперь показывает фото
первого аналога с картинкой; onError → fallback на плейсхолдер.
- SourcesProgress: источники без данных после завершения оценки
оставались в статусе idle с подписью «ожидает запрос» — выглядело
как зависший запрос. Теперь после готовности → «нет данных»
(терминальный статус).