CI frontend-tests упал: honesty-тест импортировал фикстуры из
src/app/%5F%5Fpreview/ptica/__fixtures__/ — эта dev-only директория в
.git/info/exclude, поэтому отсутствует в Linux CI checkout (vite:import-analysis
'Failed to resolve import'). Локально (macOS) резолвилось т.к. файлы на диске.
Переключил импорт на tracked src/lib/mocks/parcel-{forecast,analyze}.json
(NEXT_PUBLIC_USE_MOCKS source, в git) — report.exec_summary.key_numbers.
deficit_index присутствует, shape совместим. 138/138 vitest passed.
Refs #1871
Когда demand_normalization honesty-gate отбивает β rate-sensitivity
(`sensitivity.confidence=='low'`, ЕКБ-регрессия не проходит n≥30/R²≥0.1/slope<0),
все 393 прод-отчёта получают coefficient=1.0 для всех трёх сценариев
conservative/base/aggressive. По дизайну backend честно деградирует, но фронт
рисует 3 разноцветные карточки/линии одинаковых чисел без caveat.
Backend (scenarios.py, report.py, report_assembler.py, orchestrator.py):
- compute_scenarios теперь возвращает (list, collapsed: bool, reason: str|None)
- _detect_collapsed(): math.isclose(rel_tol=1e-9, abs_tol=1e-6) сравнивает
projected_demand_units И deficit_index на всех горизонтах между
conservative и aggressive — расхождение в любой метрике на любом h → False
- _COLLAPSE_REASON_LOW_BETA — единственный источник истины каноничного
русского предложения
- ReportScenarios получает поля scenarios_collapsed + scenarios_collapse_reason
- demand_normalization.py НЕ ТРОНУТ — поведение корректно по контракту
Backend экспортёры (report_pdf.py, excel.py):
- PDF: при collapse — тёмная плашка-параграф вместо таблицы трёх столбцов
- Excel: ОДНА строка «base» + caveat-ячейка вместо трёх идентичных столбцов
Frontend (forecast.ts, ScenariosBlock, ScenarioCards, ScenarioCompareTable,
ForecastChart, Section6Forecast, ptica.module.css):
- Тип ReportScenarios расширен двумя optional-полями
- ScenariosBlock (light): headline-bar + одна grid-карточка base + caveat
- ScenarioCards (ptica dark): sub-component ScenarioBaseCard, одна карточка
+ collapse-note, ScenarioCompareTable return null
- ForecastChart: effectiveScenarios filter — одна линия viz-1 «Базовый»
вместо трёх перекрывающихся
- Section6Forecast: caveat выше графика, читается ТОЛЬКО из
scenarios_collapse_reason (источник истины — backend constant)
Тесты: +18 новых (TestMetricsEqual×6, TestDetectCollapsed×7,
TestComputeScenariosCollapseDetection×4 + 1 проверка graceful + corner).
102 forecasting passed, 88 cross-module passed.
Refs #1871
Полный откат «Site Finder v2» с прода — пилот-беты редизайна
кокпита ПТИЦА. Удалён роут /site-finder/analysis/{cad}/v2, все
компоненты ptica-v2/* (18 файлов), беta-ссылка из AnalysisPageContent.
Сохраняется как было: старый Section1-6 на /site-finder/analysis/{cad},
PticaMapInner и старый /ptica-роут (через 301 на parent), ptica-adapt,
mocks — всё нетронуто. Прод-URL и UX не меняются для пилотов.
Старый ptica-кокпит, на основе которого делался v2, был забракован
визуально, новая попытка тоже не дошла до 1:1 с эталоном. Откат
к стабильному состоянию.
PR #1868 добавил ресурсную легенду (электр/вода/канал/тепло/газ) поверх
карты, но PticaMapInner уже отрисовывает свою POI/категорийную легенду
top-left высотой ~215px. PR #1869 попытался разнести их (top → bottom-left)
— безуспешно: моя легенда всё равно попадала в нижний хвост POI-overlay.
Прагматичный исход: ресурсная легенда ДУБЛИРУЕТ данные карточки ИНЖЕНЕРИЯ
(те же расстояния и статусы). Убираю её совсем и оставляю per-theme заголовок
карты (тот фикс был полезный и не конфликтует) — POI-overlay PticaMapInner
несёт уникальные счётчики POI, ради которых карта и нужна.
Удалены: V2MapLegend.tsx и блок CSS-правил .mapLegend* в v2.module.css.
PR #1868 поместил ресурсную легенду в top-left карты, но там уже живёт
встроенная POI-категорийная легенда от PticaMapInner (ЖК/Школы/Метро…) —
они накладывались и были нечитаемы. Перенос ресурсной легенды в bottom-left
(над Leaflet-attribution) разводит две легенды по разным углам.
— V2MapLegend overlay над cockpit-картой (электр/вода/канал/тепло/газ)
с nearest_m + статус-точка, источник utilities.summary (subtype-buckets
зеркалят backend _nearest() aliases; electric расширен power_line, gas
включает legacy generic pipeline).
— Per-theme заголовок карты: dark «СПУТНИК · СВЕЖИЙ СНИМОК», light
«СПУТНИК · СХЕМА ПОДКЛЮЧЕНИЙ» (toggle через CSS [data-theme], без JS).
— Точечный light-theme contrast в v2.module.css: kv/eng значения,
drawer eyebrow, .more / .ver / активная вкладка с cyan→accent-blue,
status-dot halo off на белом. Dark не тронут.
Scope: только ptica-v2/* и v2.module.css. PticaMapInner (шарится со
старым ptica) не тронут.
The ПТИЦА dark "operator terminal" skin was applied to the main
/site-finder map page in b3fd053 ("dark Site Finder"), but the
2026-06-21 rollback (#1865) only reverted the analysis route — the
entry-landing stayed dark.
Точечно откатывает 8 файлов entry-landing (page.tsx + 7 entry-компонентов)
до состояния b3fd053~1 и удаляет осиротевший site-finder.module.css.
НЕ трогает: PticaMapInner / ptica.module.css (используются и старым ptica,
и Site Finder v2 cockpit) — карта аналитики остаётся glyph-pin как была.
Port the approved ptica-v2 prototype to a new non-destructive route
/site-finder/analysis/{cad}/v2 — dark/light cockpit shell, real Leaflet
map (reuses PticaMapInner), 6 scan cards, lower/bottom grids, 11 drawers
and grounded chat. All values flow through the existing ptica-adapt layer
(real /analyze data or honest «—»). One beta link added on the analysis page.
The ПТИЦА dark cockpit (made default in ba05a4c) is being redesigned from
scratch. Roll the live /site-finder/analysis/[cad] route back to the original
AnalysisPageContent (Section1-6) so pilots/investor see the stable design while
the new cockpit is reworked.
Non-destructive: only the route's rendered component + metadata/fallback are
reverted. All ptica code stays in the repo and the cockpit remains viewable at
/__preview/ptica-full for redesign reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Сценарии: add section 6.6 «будущее предложение и конкуренты» (FutureSupply)
rendering report.future_market.future_competitors after ScoringTransparency
(order 6.1→6.6). Columns ЖК/Класс/Квартир/Дистанция/Поглощение, real data,
honest empty-state. Reuses .dtable.
- Отчёты: add «Параметры» panel (карта/финмодель/прогноз §22); §22 row tied to
the real forecast-ready flag. Reuses StatusRow.
Verified on the all-tabs harness; no console errors. tokens-only, no fabrication.
Один объект на avito_imv + yandex_valuation с разными ext_item_id создаёт
дубликаты в house_placement_history и double-count'ится в помесячной медиане.
Решение: CTE с DISTINCT ON (round(area_m2), floor, price, price_date) с
приоритетом avito_imv через CASE sort. Легитимные разные квартиры (разные
площадь/этаж/цена) не затрагиваются.
За флагом estimate_price_trend_dedup_enabled (дефолт True в settings).
False → точно старое поведение (backward-compat). Graceful: без изменений
в логике min_points и возврата None при недостатке данных.
Тесты: 6 новых тест-кейсов в test_estimator_price_trend_dedup.py (SQL содержит
DISTINCT ON, flag OFF не содержит, Source1 preferred, данные возвращаются корректно).
Ужесточает валидацию ДКП-сделок в _is_plausible_deal:
- floor < 1 или floor=0 → drop (floor=-5/0/999 из битого парсера)
- area_m2 задана и <= 0 → drop (нулевая/отрицательная площадь из битого парсера)
- price_rub задана и <= 0 → drop (нерыночная/техническая сделка)
- floor=None → допустимо (graceful, нечем судить)
Все новые параметры area_m2/price_rub keyword-only с дефолтом None →
backward-compatible для существующих вызовов. Caller _fetch_deals обновлён
для передачи area_m2/price_rub из данных сделки.
Тесты: 16 новых тест-кейсов в test_deals_sanitize.py (floor=-5, floor=999,
floor=None ok, area_m2=0 drop, price_rub=-1 drop и пр.).
Добавляет _is_price_sane() в cian_valuation.py: если sale_price_rub вне
[cian_valuation_min_rub, cian_valuation_max_rub] (дефолты 500k/500M),
или low_price > high_price, или любой bound отрицательный — результат
не кэшируется и возвращается None (graceful). Защита от garbage-ответов
API (999_999 < min, 9_999_999_999 > max).
Новые settings: cian_valuation_min_rub=500_000, cian_valuation_max_rub=500_000_000.
Тесты: 11 новых тест-кейсов в test_cian_valuation.py. Пофиксен pre-existing
KeyError в test_cache_hit_returns_cached (missing low_price/high_price в mock).
Prototype's first ГРАДОСТРОИТЕЛЬСТВО row is ВРИ — prod lacked it. Add it from
the real egrn.permitted_use_text (e.g. «многоквартирные дома»); honest «нет
данных ЕГРН» fallback. Value wraps cleanly in the narrow card (verified, no
overflow).
The scoped reset `.pticaRoot button { font: inherit }` had specificity (0,1,1),
beating single-class button rules like .navItem (9px), .tab (11px), .detailBtn
(9px), .ptab — so every cockpit button was forced back to the root 13px. The
oversized labels overflowed the 96px left rail (icons/text shoved right of
centre, «неровное») and bloated tabs/buttons across the cockpit.
Wrap the selector in :where(button) → specificity (0,1,0); the per-component
classes (later in source) now win their font-size, matching the prototype's
bare `button {}`. Verified on a live local render: rail labels 13→9px and every
item re-centres at 47.5px; tabs 11px; detail buttons 9px.
_save_yandex_history_items теперь пропускает items с area_m2 is None или
area_m2 <= 0 (битый парс «0,5 м²» и пр.) перед сохранением в
house_placement_history. Грязь копилась в БД и искажала price_trend.
Estimator защищён NULLIF на уровне SQL, но фильтрация на входе надёжнее.
Лог кол-ва отброшенных (info). Батч не падает из-за одного битого item.
Anchor-путь имеет hard floor от comp_min_ppm2×(1-tol). Radius-путь аналогичной
защиты снизу не имел — quarter-index-вниз или corridor-clamp могли занизить
median неоправданно (asymmetry).
Добавлен floor: если итоговый median_ppm2 < dkp_low_ppm2 × factor → поднять
до floor + лог. Только radius-путь (anchor_tier is None); без dkp_raw → no-op.
За флагами estimate_radius_floor_enabled (default True) и
estimate_radius_floor_factor (default 0.8).
_get_asking_sold_ratio теперь вызывается ПОСЛЕ anchor/IMV-blend/quarter-index/
corridor-clamp, получая ФИНАЛЬНЫЙ median_ppm2 для tier-placement (#928 audit).
До фикса: tier резолвился по pre-anchor radius-медиане (~105k), но ratio
применялся к post-anchor headline (~300k) — несовпадение tier'а в premium-
сегменте. После: ratio берётся из того же tier, что и финальный headline.
Graceful: нет ratio → expected_sold_* = None, headline не меняется.
The «Инсоляционная матрица» preview cube now gently rotates (rotateY
turntable with a constant rotateX tilt for depth) so the 3D module reads
as alive without opening the drawer. Pure CSS; honours prefers-reduced-motion.