Commit graph

324 commits

Author SHA1 Message Date
94cf1f6217 feat(concept): house-type catalog + program-driven placement (#1965 Stage 3a)
Extend the concept generator so ConceptInput can carry an optional
building_program (list of typed houses from a catalog). When present,
placement lays out EXACTLY that program — for each item, place `count`
sections of the catalog footprint at the item's floors — instead of the
greedy max-FAR coverage-cap sweep. When absent, the existing greedy
behavior is unchanged (byte-for-byte backward-compatible).

- catalog.py: hardcoded HOUSE_TYPES (panel_econom, monolith_comfort,
  tower_business, lowrise_comfort, townhouse) — sane-default catalog,
  promote to DB later; get_house_type / available_section_types lookups.
- schema: additive BuildingProgramItem {section_type, floors, count} and
  ConceptInput.building_program (default None -> greedy). ConceptVariant
  gains optional placed_count / requested_count (partial-fit signal).
- placement: shared _Placer (collision/STRtree/setback machine extracted
  from greedy sweep, reused — no duplication); place_program +
  place_program_variant; branch in place_all_strategies on
  building_program. Mixed-floor TEAP via exact per-floor-group aggregation
  (GFA = sum(area_i * floors_i), no rounding drift).
- partial fit: when the parcel can't fit all sections, place as many as
  fit and report placed_count < requested_count (no hard-422); zero-fit
  still raises ParcelGeometryError (-> 422).
- API: validate program section_type keys against the catalog (unknown ->
  422) before placement.
- tests: catalog integrity, greedy backward-compat, exact 2-item program +
  TEAP reflection, over-packed partial placement, API program path.
- regenerate frontend api-types.ts (OpenAPI codegen gate stays green).
2026-06-28 01:29:04 +05:00
a0232e22f1 feat(concept): live 3D massing → financial recompute in «7. Концепция» (#1965 Stage 2b)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m1s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
Wire the interactive 3D massing to a LIVE financial recompute inside the
Section «7. Концепция» of the light analysis report (epic #1953).

- concept-api.ts: add MassingProgram / MassingRecomputeOutput contracts +
  useRecomputeMassing() (POST /api/v1/concepts/recompute via shared apiFetch),
  and polygonCentroidWkt() helper for the DB-price fallback hint.
- MassingScene.tsx: export MassingModel + add optional onModelChange prop fired
  on the initial build and every floors/sections change (NOT on the sun/hour
  slider). No-op when absent — /ptica cockpit + preview/drawer unchanged.
- MassingEconomics.tsx (new): light-token KPI strip rendering recomputed ТЭП
  (GFA, продаваемая, квартиры, КСИТ факт/цель) + финмодель (выручка,
  себестоимость, прибыль, ROI, NPV, IRR, цена). Debounced ~250ms, latest-wins
  via monotonic request id, last-good values + subtle note on error, grey-fade
  skeleton on first load, КСИТ over-cap flag.
- Section7Concept.tsx: mount lazy MassingViewer (dynamic ssr:false) + the
  economics strip; map computeModel → MassingProgram (footprint/floors/sections
  ← model; site area ← polygon; class/type ← financial_estimate inferred,
  fallback comfort/mid_rise; land ← egrn cadastral; price/source ←
  financial_estimate; КСИТ-цель ← coerced nspd_zoning.max_far, fallback 3.5).
2026-06-28 01:01:53 +05:00
7e824ce6e4 chore(frontend): regenerate api-types.ts for /concepts/recompute (#1965 Stage 2a)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m11s
CI / backend-tests (pull_request) Successful in 14m1s
The new POST /api/v1/concepts/recompute endpoint + MassingProgram /
MassingRecomputeOutput schemas changed the backend OpenAPI; the generated
frontend/src/lib/api-types.ts was out of sync, failing the CI
openapi-codegen-check gate. Regenerated via the exact CI sequence
(openapi-typescript + prettier 3.9.0). Byte-stable on 2nd regen; tsc --noEmit clean.
2026-06-28 00:38:28 +05:00
e809063ca3 feat(site-finder): add «7. Концепция» section to light analysis report
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 58s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
Fold the generative concept generator (3 strategy variants: placement map +
ТЭП + finance) into the light analysis report (/site-finder/analysis/{cad}) as
a new section after «6. Прогноз», so Концепция lives in one place alongside the
report instead of a separate flow (epic #1953, #1965 Stage 1).

Frontend-only: reuses ConceptParamsForm / ConceptVariantsResult and the
/concepts mutation (useCreateConcept) as-is — no duplication, no backend change.
Inputs are seeded from the analysis the report already has (no re-entry of the
cadastre/polygon): polygon ← extractPolygon(geom_geojson); housing_class /
development_type ← financial_estimate inferred values; land_cost ←
egrn.cadastral_value_rub; falls back to comfort/mid_rise.

Runs on demand (heavy generative call — never auto on mount); result block
(Leaflet map) is lazy-mounted via dynamic({ ssr: false }) and the section is
collapsed by default. When financial_estimate is null (ЗОУИТ/СЗЗ/нет зоны) it
shows an honest banner but still allows a manual run, since /concepts is
independent of the financial_estimate gate. Graceful empty state when geometry
is missing/unsupported. Stage 2 (interactive 3D + /concepts/recompute) left as
a seam, not built.
2026-06-28 00:13:57 +05:00
3cd1605c5c Merge pull request 'fix(site-finder): сети и точки подключения на карте Раздела 3 (#1961)' (#1998) from fix/report-map-points-1961 into main
All checks were successful
Deploy / changes (push) Successful in 11s
Deploy / build-worker (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m36s
Deploy / deploy (push) Successful in 1m29s
2026-06-27 13:00:14 +00:00
1bbc032316 fix(site-finder): show networks & connection points on Раздел 3 map (#1961)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m3s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
The "Сети" section table summarised engineering networks and distances, but
users could not see those points on the map. Three root causes:

- Connection-points layer was always empty: NSPD engineering_structures
  (cat 36328) arrive as Polygon/MultiPolygon in EPSG:3857 (prod: 587 Polygon
  + 298 MultiPolygon, zero Point), but extractLatLon only handled Point in
  EPSG:4326. Extend it to compute the outer-ring centroid for
  Polygon/MultiPolygon and reproject 3857→4326 (frontend math, no backend
  change), so the 10+ connection points render as markers with popups.
- Map vs table coverage mismatch: the table counts "В 2 км" while the OSM
  utility-infrastructure layer fetched only 500 m (1 object inside 500 m vs
  153 within 2 km). Raise the hook default to a shared
  UTILITY_COVERAGE_RADIUS_M = 2000 so the map matches the table column.
- Coherence microcopy: clarify that the map shows the same networks as the
  table (2 km), plus NSPD connection points and protection zones.

Adds unit tests for extractLatLon polygon-centroid + 3857→4326 reprojection.

Refs #1961
2026-06-27 17:52:51 +05:00
e82761964d fix(report): human RU microcopy in Site Finder Section 6 (#1963)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m4s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
CI / backend-tests (pull_request) Successful in 13m50s
Replace dev-jargon with plain RU across the §22 forecast report (6.2/6.3/6.5
+ deficit/затоварка legend). Source of truth = backend reason strings + the
frontend RU maps; text/render-only, no scoring/forecast math touched.

Backend (source of truth):
- scenarios.py _COLLAPSE_REASON_LOW_BETA: «β rate-sensitivity не прошёл gate …»
  → «чувствительность к ставке не оценена на коротком ряде ЕКБ → один базовый
  сценарий вместо трёх».
- confidence_engine.py _coverage_factor: drop «domrf↔objective» jargon, say it
  affects будущее предложение/конкуренцию. New _history_factor: «глубина истории
  N мес» + на что влияет + связь с 6.2 (короткий ряд → один сценарий).

Frontend (both Section-6 families — live analysis page + ptica cockpit):
- Deficit legend: −1 затоварка / 0 баланс / +1 острый дефицит + actionable
  трактовка; MOI tied to «сколько месяцев район распродаёт предложение».
- 6.2 heading «Почему один сценарий, а не три» over the collapse reason.
- 6.3 render confidence.rationale + weakest-link rule («скорее завысим
  недоверие, чем недооценим риск»); FACTOR_RU gains confounded_window/
  advisory_cap; factor notes shown.
- 6.5 «Вес»→«Оценка»; overall verdict vs 0.5; «Риск избытка предложения»→
  «Запас по предложению»; §-refs moved from reason into tooltip; 6 special-
  index 1-line «что это + куда лучше» descriptions; 0.00-score reasons shown.

Tests: confidence_engine (history/coverage notes), stripSectionRefs vitest.

Refs #1963
2026-06-27 17:42:27 +05:00
16f375782b Merge pull request 'fix(report): вывести ПЗЗ-градрегламент НСПД в Раздел 1 + coerceFloat КСИТ (#1962)' (#1987) from fix/report-nspd-zoning-1962 into main
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m59s
Deploy / deploy (push) Successful in 1m10s
2026-06-27 12:22:51 +00:00
63d685a105 fix(report): уникальный React key для ВРИ-списка (дубль-VRI warning) (#1962)
All checks were successful
CI / changes (pull_request) Successful in 14s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m29s
CI / openapi-codegen-check (pull_request) Successful in 2m22s
2026-06-27 17:18:57 +05:00
a9dfe44f5c fix(tradein/auth): always-available logout button on NoAccessScreen
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
CI / openapi-codegen-check (pull_request) Successful in 1m56s
Locked/no-access users (403/denied path/expired session/trial ended) were
trapped: NoAccessScreen had no logout control, and «Выйти» lived only inside
UserMenu, which does not render on the no-access screen. They could not switch
accounts without clearing browser basic-auth manually.

- Extract logout() into shared `lib/logout.ts` (single source of truth).
- UserMenu now imports it instead of a local copy (behaviour unchanged).
- NoAccessScreen renders an always-available «Выйти» accent button for every
  variant.

Mirrored across both frontends (tradein-mvp/frontend + main frontend) to keep
the MIRROR invariant in sync.
2026-06-27 15:16:42 +03:00
58e71dab01 fix(report): surface ПЗЗ-градрегламент НСПД в Раздел 1 (#1962)
Light report (Section1ParcelInfo) only rendered the dead top-level
zoning.data_available=false (closed-PKK6 path) and never showed the
resolved nspd_zoning, so the resolved regulation (synth #1891) was
invisible to the user («мы выгружали НСПД — где они?»).

- Add «Градрегламент (НСПД)»-card to Раздел 1 (right column, под ЕГРН):
  тер.зона, КСИТ/max_far, пред. высота, этажность, коэф. застройки,
  КСИТ-ёмкость/пятно (от площади ЕГРН), источник + список разрешённых
  ВРИ зоны. Empty-state когда регламент не определён (gap-зоны / не-ЕКБ).
  КСИТ-microcopy расшифровывает жаргон на первой странице.
- New pure mapper adaptNspdRegulation (nspd-regulation.ts) reused by
  ptica-adapt so light report и /ptica остаются consistent.
- coerceFloat: max_far/max_building_pct приходят с провода СТРОКАМИ
  ("2.4"/"80") — фикс латентного бага cockpit'а (typeof===number
  глушил реальный строковый КСИТ в massing/insolation).
- Widen NspdZoning.max_far/max_building_pct to string|number, add
  main_vri; expose nspd_zoning on ParcelAnalyzeResponse.
- Unit test для field-mapping + coercion + empty-state.
2026-06-27 17:10:20 +05:00
6adba229f0 fix(site-finder): wire encumbrance/obj_class/ЗОУИТ display + forecast axis & flat-state (#1953)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
CI / openapi-codegen-check (pull_request) Successful in 1m45s
Four frontend display fixes for the Site Finder analysis report (audit #1953);
all consume fields the (already-deployed) backend now returns.

- #1954: ЕГРН «Обременения» row was hardcoded «—». Wire top-level
  `encumbrance` block {has_zouit, zouit_count, zouit_types} via
  formatEncumbrance → «ЗОУИТ: N (types)» / «не выявлено (НСПД)».
- #1955: show obj_class per success bucket («Студии 15-30 · Комфорт») so
  rows with identical area-labels are distinct; fix top-row match + React key
  to (bucket, obj_class).
- #1957: NspdZouitOverlapsBlock renders human type_zone + reg_numb_border,
  adds a СЗЗ caveat banner (СанПиН 2.2.1/2.1.1.1200-03) and «СЗЗ вашего
  участка» highlight; switched raw hex to UI tokens.
- #1958: ForecastChart axis names → nameLocation:middle + nameGap (no legend
  overlap); grey flat-state callout when the deficit line is degenerate
  (scenarios_collapsed or all points clamped at ±1).

Tests: formatEncumbrance + isDeficitDegenerate (15 new). Real segment
differentiation is backend #1959 (separate).
2026-06-27 16:51:40 +05:00
d5e51ca809 Merge branch 'main' into fix/report-data-1953
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m36s
CI / openapi-codegen-check (pull_request) Successful in 2m28s
CI / backend-tests (pull_request) Successful in 14m16s
2026-06-27 16:26:39 +05:00
ce6e693ea7 fix(ci): pin prettier 3.9.0 for codegen check + pre-commit so api-types is byte-stable
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m13s
CI / backend-tests (pull_request) Successful in 12m25s
CI / openapi-codegen-check (pull_request) Successful in 2m10s
openapi-codegen-check был RED на каждом PR: CI шаг гонял bare `npx prettier`
(резолвится в плавающий latest), а committed api-types.ts был отформатирован
старым prettier (≤3.6.2, union-типы multi-line). 3.9.0 переносит union-wrapping
на single-line → CI-регенерация всегда давала diff → gate падал.

Чиним детерминизмом одной версии на обоих путях:
- frontend/package.json: добавлен prettier@3.9.0 (exact) в devDependencies
  + обновлён package-lock.json.
- CI openapi-codegen-check: bare `npx prettier` → ./node_modules/.bin/prettier
  (project-local pinned, не плавает).
- .pre-commit-config.yaml: prettier hook additional_dependencies=["prettier@3.9.0"]
  — тот же движок, что в CI (alpha.8-обёртка байт-в-байт == prettier 3.9.0).
- api-types.ts перегенерирован prettier 3.9.0 (union-типы single-line).

Backend OpenAPI-схема не менялась — только формат. CI-регенерация теперь
байт-в-байт совпадает с committed-файлом и с выводом pre-commit hook.
2026-06-27 16:11:26 +05:00
453a1f08da fix(report): newbuild-consistent district median + obj_class dedup (#1953)
FIX A (#1955) «Что хорошо продаётся»: убран фантомный класс 'Comfort'.
- Миграция 172: v_bucket_success_score COALESCE(obj_class,'Comfort')
  → COALESCE(obj_class,'не указан'). Английский литерал заполнял 397 NULL
  и сливался отдельным классом от русского 'Комфорт' → визуальные дубли
  бакетов в UI. Источник уже канонически-русский (проверено на проде),
  synonym-mapping не нужен.
- parcels.py: obj_class протаскивается в success-ranking query + dict.
- TS SuccessRankingBucket.obj_class добавлен.

FIX B (#1960) «Медиана рынка» = 64k (квартальная росреестровская n=1 ДКП):
- district.median_price_per_m2 больше не COALESCE(median_12m, ekb_ref) в SQL.
  Basis-приоритет (newbuild-first): Objective по имени района →
  geo_radius (Objective в 3км) → ekb_districts reference →
  квартальная росреестровская медиана ТОЛЬКО при deals_count≥5.
  Для 66:41:0205010:287: 64k → 132690 (geo_radius, newbuild-consistent).
- median_price_basis добавлен в payload + TS type (nullable median).
- Frontend null-guards для нового nullable median.

Tests: +4 (geo_radius basis, objective-приоритет, deals-guard, obj_class
passthrough); обновлены district-моки в 9 analyze-тестах под новую
SQL-сигнатуру.
2026-06-27 15:10:49 +05:00
bd1ed4861c docs(zouit): уточнить DISTINCT ON-комментарий + убрать stale cad_zouit из FE-типа (#1957)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m0s
CI / openapi-codegen-check (pull_request) Successful in 1m51s
CI / backend-tests (pull_request) Successful in 12m4s
Code-review nits (без изменения поведения):
- quarter_dump_lookup.py: комментарий про дедуп ошибочно утверждал, что зоны без
  рег-номера не схлопываются. На деле reg_numb_border — TEXT NOT NULL (0 NULL на
  проде), а DISTINCT ON в PG считает NULL равными. Переформулировано на точное.
- frontend/types/nspd.ts: убран устаревший 'cad_zouit' из списка возможных
  group_key (бэкенд теперь эмитит только protected/engineering/okn/natural/other).
2026-06-27 14:21:01 +05:00
854fa69a8b feat(site-finder): render OSM utility-infrastructure layer on analyze map (#1746)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 54s
CI / openapi-codegen-check (pull_request) Successful in 1m49s
Section2 "Сети" map now shows the OSM engineering-networks layer
(osm_utility_infrastructure_ekb, endpoint /parcels/{cad}/utility-infrastructure)
alongside the NSPD connection-points + ЗОУИТ layers from #1751, on the same map.

- useUtilityInfrastructure hook (react-query, mirrors useConnectionPoints)
- UtilityInfrastructureLayer: Point→CircleMarker, LineString/Polygon(+Multi)→GeoJSON,
  per-kind colour (power/water/gas/heat/communication/sewage), popups
- UtilityLayerControlPanel: collapsible legend, per-kind toggle + count
- SiteMap: optional utilityInfrastructure prop + visibleKinds state (CP/ЗОУИТ unchanged)
- 12 unit tests (geometry parsing + [lon,lat]→[lat,lon] swap + grouping)

Refs #1746
2026-06-27 03:40:51 +05:00
3753079dee feat(site-finder): OSM engineering-networks loader + endpoint (#1746) (#1930)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m53s
Deploy / build-worker (push) Successful in 3m6s
Deploy / build-frontend (push) Successful in 3m19s
Deploy / deploy (push) Successful in 1m26s
2026-06-26 20:01:00 +00:00
b82963d12e feat(financial): ground-floor нежилое (office/commercial) tranche
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m50s
Deploy / build-worker (push) Successful in 3m0s
Deploy / build-frontend (push) Successful in 3m18s
Deploy / deploy (push) Successful in 1m22s
Carve a documented office share (comfort/business 5%, econom 0) out of GFA before residential — fixes prior overstatement (all non-parking GFA = residential). total_floor_area unchanged → no double-count; office is additive revenue (+15% premium), нежилое → VAT-able (vat base = parking_va + office_va). Σ invariant holds. Additive schema + api-types regen + PDF/concept display. Deep-review . Refs #1881
2026-06-25 11:40:01 +00:00
55811ff7cc feat(workers): data-freshness monitor (Celery beat → GlitchTip)
All checks were successful
Deploy / changes (push) Successful in 15s
Deploy / build-backend (push) Successful in 1m58s
Deploy / build-worker (push) Successful in 3m9s
Deploy / build-frontend (push) Successful in 3m19s
Deploy / deploy (push) Successful in 1m24s
Extract compute_freshness(db) from the /admin/scrape/freshness handler (endpoint unchanged) + daily beat task scrape_freshness_check that alerts via sentry_sdk.capture_message when a source is stale/failed (error on critical-failed, else warning). Registered in celery include + beat (09:00 MSK). Refs #73
2026-06-25 11:29:34 +00:00
39f099bc17 chore(mocks): regenerate parcel-analyze financial_estimate (consistent with current backend)
All checks were successful
Deploy / build-frontend (push) Successful in 3m29s
Deploy / deploy (push) Successful in 1m11s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / changes (push) Successful in 7s
Regenerate the drifted dev fixture financial_estimate via compute_financial() from its own teap_synth inputs — parking comfort-priced (#1893), VAT parking-only (#1898), levered_irr refreshed (#1900). Dev-only. Refs #1881
2026-06-25 11:02:36 +00:00
d589ddf6c4 feat(financial): levered (equity) IRR — capstone финмодели #1881
All checks were successful
Deploy / build-worker (push) Successful in 3m8s
Deploy / deploy (push) Successful in 1m30s
Deploy / build-frontend (push) Successful in 3m25s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m53s
Levered (equity) IRR from the LTC debt-schedule equity cashflow, mirroring the unlevered IRR + roi-proxy fallback. Honest negative-leverage (can be below project IRR). Σ equity_cf == net_profit_after_financing invariant holds. Unlevered untouched. Additive schema fields (api-types regenerated). Cockpit drawer + concept display. Deep-review .

Refs #1881
2026-06-25 09:06:46 +00:00
dc659da655 feat(site-finder): geo-radius market-price calibration in /analyze (#1881 follow-up)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 2m1s
Deploy / build-worker (push) Successful in 3m12s
Deploy / build-frontend (push) Successful in 3m34s
Deploy / deploy (push) Successful in 1m24s
New price tier objective_geo_radius: ST_DWithin median of Objective new-build prices (objective_lots ⋈ complexes) within 3km of parcel centroid, between quarter-MV and district_reference. Closes the name-match gap (5 of 9 EKB districts had no Objective name-match). data/sql/168 functional GIST index (prod EXPLAIN 114ms→28ms). Degenerate-centroid guard + honest RU price_source captions. Deep-review .

Refs #1881
2026-06-25 07:59:56 +00:00
3073651812 fix(generative/financial): VAT only on parking, residential ДДУ exempt (ст. 149 НК)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m48s
Deploy / build-frontend (push) Successful in 3m24s
Deploy / deploy (push) Successful in 1m30s
Deploy / build-worker (push) Successful in 3m40s
VAT charged on full gross margin → only on parking value-added; residential (реализация жилья + услуги застройщика по ДДУ) exempt per пп.22/23.1 п.3 ст. 149 НК; input VAT embedded in gross СМР. Σ cashflow == net_profit invariant preserved. PDF + concept-UI caveats synced. +3 tests, deep-review .

Refs #1881
2026-06-25 07:22:58 +00:00
4bb09911b8 feat(financial): LTC 70%/equity 30% норматив МКД РФ в модели финансирования
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 2m35s
Deploy / build-frontend (push) Successful in 3m30s
Deploy / build-worker (push) Successful in 4m6s
Deploy / deploy (push) Successful in 2m19s
Заменяет 100%-кредитное покрытие разрыва реалистичной LTC-моделью (банк 70%, собственные средства 30%). total_interest снижается ~30%, net_profit_after_financing растёт. Backend 41/41, Frontend 20/20.
2026-06-24 16:49:01 +00:00
5a73f1d0b6 feat(cockpit): поверхностить financial_estimate в Development Scan / Investment Clearance (#1881 rank-5) (#1894)
All checks were successful
Deploy / build-backend (push) Has been skipped
Deploy / changes (push) Successful in 7s
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m18s
Deploy / deploy (push) Successful in 1m10s
feat(cockpit): поверхностить financial_estimate в Development Scan / Investment Clearance (#1881 rank-5)
2026-06-24 16:20:19 +00:00
b6b9aa9929 feat(financial): DCF-график продаж по реальному темпу поглощения (rank 1)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m10s
CI / openapi-codegen-check (pull_request) Successful in 2m3s
CI / backend-tests (pull_request) Successful in 10m10s
Финмодель брала фиксированное окно продаж 30 мес независимо от рынка
(schedule_is_default всегда true). Теперь окно считается из ЛОКАЛЬНОГО темпа
поглощения, который уже вычисляется в том же /analyze, но финмодель его
игнорировала.

Корректность абсорбции (ключевое): velocity.monthly_velocity_sqm — это СУММА
поглощения ВСЕГО конкурентного набора в радиусе (м²/мес), НЕ темп одного
проекта. Поэтому per-project absorption = monthly_velocity / max(n_with_sales,1)
(темп одного типичного локального продавца) — иначе модель считала бы, что новый
проект забирает весь рыночный темп (дико оптимистично). Поле
project_absorption_sqm_per_month добавлено в VelocityResult (objective-путь);
rosreestr-fallback и вырожденные пути → None (поквартальный count без
по-проектной декомпозиции не может задавать график).

financial.py: окно = clamp(ceil(residential/velocity), MIN=6, MAX=120) при
конечной velocity>0; иначе дефолт 30. Эскроу-инвариант сохранён:
sales_end=max(sales_start+base, constr_end). Инвариант Σ cashflow == net_profit
держится (перенос выручки во времени не меняет сумму). schedule_is_default
флипается в false когда график рыночный; новое поле sales_duration_months
(реализованное окно) для UI/PDF.

Wiring: parcels.py → synthesize_parcel_financial(velocity_sqm_per_month) →
compute_financial(market_velocity_sqm_per_month). Generative §1c путь пока
передаёт None (out of scope, follow-up).

Тесты: +13 (None→дефолт+инвариант; рыночная velocity; клампы MIN/MAX; эскроу;
non-finite→fallback; rosreestr→None; инвариант по размерам окна; регресс PR-3 —
ровно одна смена знака на коротком окне). Полный бэкенд: 3414 passed, 0 failed.
ruff+mypy(strict financial.py) чисто. api-types перегенерены.

Code-review: 2× approve, 0 majors (adversarial correctness-lens подтвердил
семантику абсорбции, инвариант, не-proxy IRR, клампы, rosreestr-None).

Refs #1881

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 02:56:48 +05:00
c972eebce6 chore(api-types): regen после нового admin-endpoint backfill (#1881)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m6s
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 10m12s
openapi-codegen-check: новый POST /admin/scrape/zone-regulations/backfill +
TriggerZoneRegulationsBackfillRequest добавили path/schema в OpenAPI →
api-types.ts должен байт-в-байт совпадать с генератором.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 02:17:04 +05:00
5359556068 chore(mocks): add financial_estimate to parcel-analyze mock for local preview (#1881)
All checks were successful
CI / openapi-codegen-check (pull_request) Successful in 1m50s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
Финмодель (PR-1..5) видна в кокпите только при NEXT_PUBLIC_USE_MOCKS=true если
мок несёт financial_estimate (моки минуют backend → фронт читает JSON напрямую).
Раньше мок его не имел → Investment Clearance в локальном preview показывал «—».

Добавлен аутентичный financial_estimate (37 полей: каскад затрат + БДР + DCF
NPV/IRR/PBP + финансирование + teap_synth + caveat), сгенерённый РЕАЛЬНОЙ
synthesize_parcel_financial для участка area=8240, КСИТ 3.5, цена 165k/comfort/
high-rise — гарантирует совпадение с прод-выводом, не руками-числа. NPV 3.9М,
IRR 15.4%, чистая прибыль после финанс. 7.2М (проценты 224М съели маржу —
реалистичный тонкий high-rise).

Refs #1881
2026-06-24 01:58:44 +05:00
d350d27325 feat(finmodel): financing overlay (кредит 15%) поверх unlevered DCF (epic #1881 PR-5)
All checks were successful
CI / frontend-tests (pull_request) Successful in 59s
CI / openapi-codegen-check (pull_request) Successful in 1m53s
CI / backend-tests (pull_request) Successful in 10m1s
CI / changes (pull_request) Successful in 7s
Финансирование как ДОПОЛНИТЕЛЬНЫЙ overlay поверх готового unlevered cashflow:
кредитная линия покрывает накопительный кассовый разрыв, проценты капитализируются
(compound), ставка _CREDIT_RATE_ANNUAL=15% (норматив, ОТДЕЛЬНО от ставки
дисконтирования — разная семантика: стоимость долга vs требуемая доходность).

Backend (financial.py):
- _compute_financing(operating_cf, annual_rate) — чистая (READ-ONLY вход, не
  мутирует): процент на остаток на начало месяца ДО движений → капитализация;
  draw=-cf при оттоке, repay=min(cf,debt) при притоке (долг не уходит в минус).
  Возвращает peak_debt_rub / total_interest_rub / ending_debt_rub.
- compute_financial: вызов после DCF, net_profit_after_financing = net − interest.
- _build_monthly_cashflow и unlevered NPV/IRR/PBP НЕ ТРОНУТЫ → инвариант
  Σ unlevered cashflow == net_profit сохранён (регресс-тест).
- FinancialModel +6 полей (financing_enabled/annual_rate_used/peak_debt_rub/
  total_interest_rub/net_profit_after_financing_rub/financing_is_simplified).

Frontend: ParcelFinancialEstimate +6 полей; adaptFinanceDrawer + concept-каскад
показывают пиковый долг/проценты/чистую прибыль после финанс. (когда enabled).

MVP: только агрегаты, БЕЗ levered IRR (хрупко на хардкод-100%-долге — отдельный
PR). HEAVY caveat: проценты — реальная стоимость (net после < net до); 100%
покрытие разрыва (нет equity/LTC); compound; эскроу не моделируется; unlevered
headline не затронут. financing_is_simplified=True флаг.

mypy strict clean, +9 backend + 3 frontend тестов (compound точное значение,
no-mutation, инвариант-регресс, монотонность, net_after<net), api-types
регенерён авторитетно, deps не тронуты (hand-roll).

Refs #1881
2026-06-24 01:12:47 +05:00
67b65c8d8a feat(site-finder): bridge финмодели (DCF) в кокпит Investment Clearance (epic #1881 PR-4)
All checks were successful
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Successful in 9m54s
Investment Clearance в site-finder кокпите больше не «—»: синтезируем лёгкий
ТЭП из buildability участка (площадь + предельные параметры зоны НСПД/ПЗЗ —
КСИТ/%застройки/этажность) → прогоняем через полноценную финмодель (каскад
затрат PR-1 + калибр.цена PR-2 + помесячный DCF PR-3) → GDV/Cost/Profit/ROI/
IRR/NPV/PBP в кокпите.

Backend:
- new app/services/site_finder/parcel_financial.py (ЧИСТЫЕ функции, без БД):
  synthesize_teap_from_buildability (GFA=area×max_far, degradation pct+floors;
  residential=GFA×eff; apartments/parking — нормативы teap.py, не дублируются)
  + synthesize_parcel_financial (housing_class из цены, development_type из
  этажей, land_cost=кадастровая). None когда нельзя строить МКД / нет зонинга /
  нет площади.
- parcels.py: 1 вызов в try/except (hot-path-safe → None при сбое) + ключ
  financial_estimate в /analyze. Схема не тронута (AnalyzeResponse extra=allow,
  codegen не нужен). +18 тестов.

Frontend:
- ParcelFinancialEstimate тип (site-finder.ts, hand-typed).
- adaptInvestmentClearance(financial)/adaptFinanceDrawer(financial): реальные
  числа (GDV/Cost/Profit/ROI/IRR + NPV + срок окуп.) или честный placeholder
  когда null. Локализация класса/типа (high_rise→высотная). Прокинут analysis
  через PticaBottomGrid/drawer-registry. +6 тестов, 144 passed.

HEAVY caveat везде: ОРИЕНТИРОВОЧНАЯ модель по МАКС.застройке НСПД, НЕ реальная
концепция; land=кадастровая (не рыночная); график — типовой; IRR proxy-флаг.
mypy strict clean (generative.*), ruff/tsc/lint clean.

Закрывает эпик #1881 (финмодель = полноценная DCF, видна в кокпите).
Follow-up: финансирование (кредит/займы), §22 sales-pace, гео-радиус калибровки.

Refs #1881
2026-06-23 22:44:04 +05:00
d4068a255b feat(finmodel): monthly DCF — real NPV/IRR/PBP, replace IRR-proxy (epic #1881 PR-3)
All checks were successful
CI / frontend-tests (pull_request) Successful in 57s
CI / backend-tests (pull_request) Successful in 9m54s
CI / changes (pull_request) Successful in 6s
CI / openapi-codegen-check (pull_request) Successful in 1m53s
Hand-rolled детерминированный DCF (без numpy-financial/новых deps): _npv /
_irr_monthly (bisection с bracket-guard) / _payback_months над помесячным
cashflow, построенным из статического каскада (PR-1) + калиброванной цены
(PR-2) по дефолтным нормативам фаз (ПИР 6мес → СМР по development_type
24/36/48 → распродажа, дисконт 15%).

- Налоги прорейтятся на признание выручки → Σ недисконтированного cashflow ==
  net_profit (ИНВАРИАНТ, тест — гарантия что DCF не разошёлся со статикой).
- IRR настоящий когда есть смена знака; вырожденный поток → fallback на
  roi-proxy (irr_is_proxy=True). Честно помечено.
- График продаж привязан к завершению стройки: sales_end = max(sales_start +
  длительность, construction_end) — продажи не заканчиваются раньше ввода
  (РФ ДДУ/эскроу). Без этого high_rise распродавался за 12 мес ДО ввода →
  двойная смена знака → profitable-проект молча падал в proxy + инверсия NPV.
- FinancialModel +npv_rub/payback_months/discount_rate_used/schedule_is_default
  (additive). development_type проброшен через placement.
- UI/PDF: NPV/IRR/PBP + честный caveat «график — типовое допущение, точность
  зависит от него». api-types регенерён авторитетно.

mypy strict clean (generative.*), +15 тестов (DCF helpers на учебных значениях,
ИНВАРИАНТ, IRR real-vs-proxy обе ветки, PBP, sales≥construction для всех типов,
profitable high_rise → настоящий IRR). 48 passed.

Refs #1881
2026-06-23 21:52:04 +05:00
b1332d6b55 feat(generative): calibrate finmodel sale price from Objective market data (epic #1881 PR-2)
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m3s
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 10m6s
Заменяет хардкод-цену продажи жилья (класс-норма) на реальную рыночную
медиану из Objective по локации участка. compute_financial остаётся ЧИСТЫМ
(без БД) — DB-lookup в API-слое, цена прокидывается параметром.

- compute_financial: +optional market_price_per_sqm/price_source; цена =
  рынок если есть, иначе класс-норма. Паркинг/СМР НЕ калибруем (только жильё).
- concepts.py _lookup_market_price(db, lon, lat): центроид → ближайший
  ekb_district (ST_DWithin 5км) → медиана objective_lots.price_per_m2_rub
  (n>=10, фильтр 30k-600k) → fallback ekb_districts.median_price_per_m2 →
  None. try/except → graceful (None, class_norm) при любой ошибке (вне ЕКБ/
  нет гео/SQL) без краха генерации. psycopg3 CAST.
- FinancialModel +price_per_sqm_used/price_is_calibrated/price_source (additive).
- Threading market_price через generate→placement→compute_financial (optional
  kwargs, backward-compat).
- UI/CSV: честный caption источника (рынок Objective / справочник района /
  норматив класса). Старый лживый footnote «не калиброванная модель» → условный.

Prod-verified: калибруется 4 главных ЕКБ-района по name-match (Академический
204k лотов, Ленинский 38k, Кировский, Орджоникидзевский); остальные 5 admin-
районов честно → district_reference. Гео-радиус matching (полное покрытие) —
follow-up.

api-types.ts регенерён авторитетно. mypy strict clean (generative.*), +14
тестов (калибровка/lookup 4 ветки/SQL-ошибки graceful/backward-compat).

Refs #1881
2026-06-23 21:10:25 +05:00
65bab902e1 feat(generative): full static developer financial cascade (epic #1881 PR-1)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m5s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 10m10s
Объединяет наше (площади из реальной геометрии teap) + полноту девелоперской
Excel-модели (каскад затрат + БДР с налогами). Расширяет financial.py из
статического GDV−COST в полный статический расчёт. БЕЗ DCF (PR-3), БЕЗ новых
вводных от пользователя — только нормативы + существующий TEAP.

Каскад (financial.py):
- Выручка: жильё (residential×price) + ПАРКИНГ (spaces×1.9М)
- Затраты: СМР(резид+паркинг) + ПИР 2.5% + сети/ТУ 6000₽/м² + услуги заказчика
  1.5% + резерв 3% + маркетинг/риэлтор 7% выручки + земля
- БДР: валовая маржа → НДС (упрощённо) → прибыль до налога → налог 25% (ФЗ
  176-ФЗ) → ЧИСТАЯ прибыль → ROI + чистая маржа%
- Нормативы документированы (источник: Excel-эталон «Авангардная 13» + RU-рынок)

FinancialModel (concept.py): +16 additive-полей (revenue breakdown, cost cascade,
БДР/налоги, roi, margin_pct, irr_is_proxy). Старые 4 поля сохранены (backward-compat).

IRR честно помечен irr_is_proxy=True (annualized net ROI, не DCF) — настоящий
IRR/NPV/PBP в PR-3 (помесячный cashflow). НДС — документированное упрощение, не
точный НК РФ. Офисы пропущены (нет площади в TEAP).

PDF + concept UI: полный БДР-каскад + чистая прибыль/ROI + честные сноски.
api-types.ts регенерён авторитетно (openapi-typescript + prettier, как CI).

mypy strict clean (generative.*), +тесты (каскад/паркинг/налоги/backward-compat/
edge cost=0), 36 passed.

Refs #1881
2026-06-23 16:45:58 +05:00
f7773c71d4 fix(site-finder): P3 a11y/UI batch from audit #1871
All checks were successful
CI / openapi-codegen-check (pull_request) Successful in 1m46s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 48s
- add <h1> page title to analysis page (иерархия стартовала с h2 — a11y)
- emoji → Lucide во всех вердикт/баннер/статус UI (ui-conventions: emoji
  запрещены): GateVerdictBanner (VerdictColor.icon string→ComponentType,
  sourceHint→ReactNode, SectionLabels), + найдены ещё 3 файла:
  OverviewTab (⚠ ЛЭП/трамвай), GeotechRiskBlock (🟡 вечная мерзлота → Snowflake,
  ⚠ загрязнение), HydrologyBlock (⚠ паводок, 🏞💦🪷🚣💧 subtype-map → Waves/Droplet).
  Все иконки декоративные → aria-hidden, семантика дублируется текстом.
  Семантические цвета через var(--success/--warn/--danger) + hex-fallback.
- расшифровка аббревиатур при первом упоминании (ui-microcopy):
  Section2 «строительство в охранной зоне (ОЗ)... зоне с особыми условиями
  использования территорий (ЗОУИТ), тип 5»; Section4 title-tooltip для ЗОУИТ.
- ForecastChart: axis-chrome hex (:65/67/68) задокументирован как canvas-renderer
  исключение (echarts canvas не резолвит CSS var() — var() сломал бы chrome,
  как и VIZ-series). Цвета не тронуты, только комментарий.

НЕ в scope: ForecastChart CI-band (±p25/p75 — backend не отдаёт, отдельная
задача); ★ в WeightProfilePanel <option> (SVG в option невозможен);
токенизация legacy-hex в Geotech/Hydrology (отдельный pass).

138 frontend vitest passed, tsc/lint clean. lucide-react уже в deps.

Refs #1871
2026-06-23 12:52:32 +05:00
589b3bfdc3 fix(site-finder): honest POI-score label + drop duplicate MOI column (audit #1871 P2)
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m0s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI / backend-tests (pull_request) Successful in 9m59s
CI / changes (pull_request) Successful in 6s
Два disclosure-фикса + 3 consistency follow-up (по code-review).

invest_score = взвешенная POI-сумма (distance-decay + center-bonus, max-ref
40), НЕ калиброванный инвест-рейтинг (scorer.py — пустой стаб). Был мислейбл
«Инвест-балл». Формулу/SCORE_MAX_REFERENCE не трогаем — только честная подпись.
- backend parcels.py: score_explanation → «POI-скор (0–40, оценочный)...
  Не инвест-рейтинг...»
- KpiCard: новый optional caption prop (backward-compat).
- Section1ParcelInfo: KPI «Инвест-балл» → «POI-скор · 0–40» + caption «оценочный».
- legacy site-finder: scoreLabel → «POI-скор · {label} · оценочный».
- CompareTable: «Инвест-балл» → «POI-скор · 0–40» (тот же score, был мислейбл).
- landing app/page.tsx: «Score 0–100» → «POI-скор 0–40» (реальная шкала).
- Section1 scoreColor: пороги 0–100 → 0–40 калиброванные (_score_label:
  <5 плохо · 5–15 средне · 15–25 хорошо · ≥25 отлично) — раньше хороший
  ~30/40 участок рисовался red рядом с честным «0–40» лейблом.

MOI (months_of_inventory) математически КОРРЕКТЕН — по определению не зависит
от горизонта проекции (MOI = сток / месячный темп). Не баг. Но дублировался
колонкой на 4 горизонта в ForecastHorizonsBlock (116.6/116.6/117.6/117.6 —
выглядело сломанным). Убрал колонку; MOI остаётся 1 раз в Section6 key_numbers.
deficit_index (варьируется) сохранён. Подчищены unused balanced/DEFICIT_BALANCE_EPS.

138 frontend vitest passed, ruff/tsc/lint clean.

Refs #1871
2026-06-23 12:29:02 +05:00
adf3ec7c00 fix(site-finder): velocity fallback 4500→750, coverage/proxy disclosure, market_trend staleness honesty (audit #1871 P2)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m5s
CI / openapi-codegen-check (pull_request) Successful in 1m59s
CI / backend-tests (pull_request) Successful in 10m9s
Три honesty/correctness-фикса из audit-issue #1871 (🟠 P2). Все факты
подтверждены prod-DB + разведкой. ETL не трогаем (отдельная задача).

velocity.py:
- _EKB_MEDIAN_FALLBACK 4500→750 (audit-verified медиана ЕКБ 593-766;
  4500 завышало знаменатель нормализации 7.6× → занижало velocity_score
  при DB-error fallback, latent). Консервативно 750 (верх диапазона).
- VelocityResult + as_dict() раскрывают: objective_coverage_pct,
  proxy_used, proxy_sqm_per_deal — теперь видно что объём rosreestr-пути
  фабрикуется count×45 м²/сделка при низком покрытии Objective (<50%).
  Проброшено во все 4 конструктора, additive.

parcels.py (market_trend producer):
- Различать «источник устарел» от «нет сделок». rosreestr_deals
  MAX(period_start_date)=2026-01-01 (stale 5.5 мес) → recent-окно пусто →
  раньше голый null. Теперь status ∈ {ok, source_stale, no_deals} +
  as_of_date + label + recent_deals_count. MAX(deal_date) добавлен в CTE.
  Consumer scoring читает recent_deals_count защищённо (без регрессии).

frontend (types + MarketTrendBlock + VelocityBlock + ptica market-drawer
+ Section3 verdict + legacy guard):
- staleness/no_deals → честный caveat вместо фейкового тренда.
- velocity → caption «объём оценочный (прокси N м²/сделка, покрытие P%)».
- formatAsOfMonth парсит YYYY-MM напрямую (TZ-safe).
- Типы MarketTrend/Velocity расширены (optional, backward-compat;
  отсутствие status = "ok").

Тесты: +3 velocity unit + extended as_dict contract. 17 velocity passed,
138 frontend vitest passed.

Refs #1871
2026-06-23 12:04:08 +05:00
389bdd3306 test(ptica): use tracked mock fixtures instead of git-excluded __preview (CI fix)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 54s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m54s
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
2026-06-23 11:25:33 +05:00
2303207629 fix(ptica): honest isReal flags — placeholder stubs no longer marked real (audit #1871 P1.3)
Some checks failed
CI / frontend-tests (pull_request) Failing after 53s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m52s
Adapter — единственный источник истины real-vs-placeholder; стаб с isReal:true
рисуется как живое измерение (обычная типографика «—», неотличимо от данных).
Helpers real()/placeholder()/notReal() корректны — баг в их misuse на 5 местах.

ptica-adapt.ts:
- :163 encumbrances (passport-card): {value:PLACEHOLDER, isReal:true} → notReal("источник ЕГРН")
- :272 buySignalFromForecast при deficit_index==null: {value:"наблюдать", isReal:true}
  → notReal("прогноз недоступен (Сценарии §22)") — отличает «прогноза нет» от
  «прогноз говорит наблюдать». di-present advisory-ветка (real) не тронута.
- :1942 adaptBuySignalGauge при di==null: убран synthetic gaugeValue=50 как real;
  ранний if(di==null) зеркалит no-report ветку (value:null, isReal:false, available:false).
- :681 adaptPassportDrawer registry, :1048 adaptLegalDrawer registry: те же
  «Обременения» стабы {value:DASH, isReal:true} → notReal (найдены code-review,
  recon их пропустил — drawer-зеркала card-фикса).

ptica.module.css: .kvPlaceholder += opacity:0.7 — усиленный визуальный mute стаба.

Корректные места НЕ тронуты (recon-verified): market demand-label (условный
real/placeholder), engineering utilities, InvestScore potential.

+5 unit-тестов (ptica-adapt.honesty.test.ts): encumbrances stub, buy-signal di=null,
buy-signal real di (защита от over-downgrade), gauge di=null, оба drawer-обременения.

Refs #1871
2026-06-23 11:20:48 +05:00
dcccebf2c2 fix(forecasting): honest scenarios collapse when β rate-sensitivity gate fails (audit #1871 P1)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m11s
CI / backend-tests (pull_request) Successful in 10m19s
Когда 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
2026-06-22 19:06:22 +05:00
470c6b1f32 revert: remove Site Finder v2 cockpit (PR #1866/#1868/#1870)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / frontend-tests (pull_request) Successful in 56s
Полный откат «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 с эталоном. Откат
к стабильному состоянию.
2026-06-22 15:33:25 +05:00
49a84ba9f6 fix(site-finder/v2): remove resource legend overlay (overlapped POI legend)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m0s
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 2m2s
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.
2026-06-22 11:48:55 +05:00
2bdbb681b9 fix(site-finder/v2): move resource legend to bottom-left
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / frontend-tests (pull_request) Successful in 53s
PR #1868 поместил ресурсную легенду в top-left карты, но там уже живёт
встроенная POI-категорийная легенда от PticaMapInner (ЖК/Школы/Метро…) —
они накладывались и были нечитаемы. Перенос ресурсной легенды в bottom-left
(над Leaflet-attribution) разводит две легенды по разным углам.
2026-06-22 11:37:10 +05:00
c3e091c811 feat(site-finder/v2): resource legend overlay + light theme polish
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 52s
CI / openapi-codegen-check (pull_request) Successful in 1m48s
— 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) не тронут.
2026-06-22 11:26:13 +05:00
7cb008c551 revert(site-finder): rollback dark entry-landing skin to light
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 57s
CI / changes (pull_request) Successful in 6s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
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 как была.
2026-06-22 11:08:44 +05:00
eb7290dcf0 feat(site-finder): add Site Finder v2 cockpit route (ПТИЦА v2)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 53s
CI / changes (pull_request) Successful in 5s
CI / openapi-codegen-check (pull_request) Successful in 1m51s
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.
2026-06-21 22:29:18 +05:00
aba4e61be2 revert(ptica): restore old Section1-6 analysis view as default route
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m50s
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>
2026-06-21 18:39:49 +05:00
e8d0863996 feat(ptica): close tab prototype-parity gaps (Сценарии 6.6 + Отчёты Параметры)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
- Сценарии: 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.
2026-06-21 11:34:11 +05:00
28b853bcdf feat(ptica): add ВРИ row to Градостроительство (real ЕГРН permitted_use)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m4s
CI / openapi-codegen-check (pull_request) Successful in 2m4s
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).
2026-06-21 04:09:36 +05:00
b3fd053e3d feat(ptica): map glyph pins + compass, equal-height grids, full-height rail; dark Site Finder
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 55s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
ПТИЦА cockpit:
- map: POI/competitors/connections now render as glyph divIcon pins (per-category
  glyph + color) with dashed connection lines + a compass — matches the prototype
  (was plain CircleMarker dots). Legend/zoom/scale/base-toggle/tools kept.
- lower+bottom grids: align-items stretch → equal-height rows; cards are flex
  columns with footer «Подробнее» pinned to bottom and sparse empty-states
  centred (fixes «выровняй тут все»).
- rail: dropped sticky+height:100vh so it stretches to full page height like the
  prototype (fixes «обрезан»).

Main Site Finder (entry landing) — restyled to the ПТИЦА dark cockpit theme,
scoped to .sfRoot[data-theme=dark] (no leak to light pages): dark CARTO tiles,
Leaflet attribution flag removed (prefix=false, © credit kept), dark markers /
filter chips / legend / recent list / drawer / cad input.
2026-06-21 03:41:13 +05:00