Commit graph

148 commits

Author SHA1 Message Date
e67cb721bf fix(objective): physflat-dedup current-state consumers + honest n_sold window (#1964)
objective_lots — current-state UPSERT (UNIQUE objective_lot_id, 5 snapshot_date),
но Объектив присваивает ОДНОМУ физлоту (project,corpus,section,floor,lot_number)
несколько lot_id за пере-листинги → таблица раздута ~2.91× (прод: 1.75M квартир-
строк vs 603k физлотов). История — в отдельной objective_lots_history (не трогаем).

STEP 1: миграция 175 — VIEW v_objective_lots_latest (DISTINCT ON physflat-ключ,
последний снапшот). ol.* стабилен (51==51 колонок). DRY-RUN на проде: 603 049
квартир vs 1 753 283 raw.

STEP 2: репойнт current-state консьюмеров objective_lots → v_objective_lots_latest:
- supply_layers._L1_OPEN_SQL (L1 открытое предложение → дефицит-форсайт; прод:
  Юго-Западный комфорт 58 606 → 12 620)
- competitors._SOLD_COUNT_SQL (+ комментарий: COUNT(DISTINCT lot_id) СОХРАНЁН для
  fan-out-защиты маппинга, не COUNT(*) — view гарантирует physflat-дедуп, DISTINCT
  гарантирует mapping-fan-out-safety)
- parcels.py obj_pricing CTE (карточка конкурента units_sold/available)
- special_indices._ARTIFICIAL_DEMAND_SQL
- parcels.py district price block + geo-radius median (sample_size/n)
- concepts._OBJECTIVE_MEDIAN_SQL (гейт n≥10)
- landing KPI3 (% квартир с ценой)
- admin_scrape coverage: `lots` оставлен сырым (ETL-fidelity vs SQLite), добавлен
  `lots_physflat`
#1959 inline-дедуп в market_metrics НЕ рефакторим — добавлен комментарий об общем
physflat-ключе с view.

STEP 3: report_assembler._deal_count теперь = unit_velocity × window_months
(оконные продажи), НЕ кумулятивный n_sold. confidence_engine помечает фактор
«за 6 мес» и гейтит порогами окна (high≥50) — кумулятив (прод EKB ~380 921) делал
гейт бессмысленным и подпись лживой; оконное (~24 876 за 6 мес) честно.

Тесты: +guard'ы (L1/sold-count читают view, deal_count оконный). 963 passed.
2026-06-28 04:27:43 +05:00
8aec625134 fix(audit): index for flats_latest, coerce max_height_m, hide empty reg-date (#1953)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Successful in 1m2s
CI / openapi-codegen-check (pull_request) Successful in 1m56s
CI / backend-tests (pull_request) Successful in 14m7s
Three #1953 audit follow-ups:

- perf: add migration 174 — composite index idx_kn_flats_obj_snap on
  domrf_kn_flats (obj_id, snapshot_date DESC). Serves the #1956 flats_latest
  CTE in best_layouts.py (DISTINCT ON (obj_id) ORDER BY obj_id, snapshot_date
  DESC + self-join on (obj_id, snapshot_date)); previously only (obj_id)
  existed so Postgres sorted per object. Prod: 789 569 rows, idx ~5.7 MB,
  dry-run instant. Idempotent, self-wrapped BEGIN/COMMIT.

- frontend: route every max_height_m read through coerceFloat (same string-bug
  class as max_far #1962). max_height_m is NUMERIC → arrives as a string on the
  wire; ptica-adapt.ts read it raw at 4 sites and relied on formatInt/Math.round
  coercion. Widen the type in nspd.ts and fix the stale "real number" comment in
  nspd-regulation.ts.

- frontend: hide the «Дата регистрации» EGRN row entirely when
  registration_date is null (~97% of parcels) instead of rendering a bare «—».
2026-06-28 03:19:18 +05:00
41804ed70e fix(forecast): посегментный+дедуплицированный индекс дефицита (#1959)
Корень «−1.00 везде» (эпик #1953): compute_demand_supply_forecast брал
district-wide unit_velocity (847.5/мес, ВСЕ классы/комнаты) как спрос и
весь district-сток (~63k доступных) как предложение для КАЖДОЙ ячейки
what_to_build → один и тот же ratio во всех ячейках → все deficit_index
прижаты к −1.0. Плюс objective_lots — append-per-snapshot (~2.9× инфляция
строк), что симметрично раздувало обе базы → даже сегментация без дедупа
осталась бы вырожденной.

Фикс (blast radius — ТОЛЬКО forecast/deficit calc; platform-wide dedup = #1964):
- market_metrics.compute_market_metrics: +obj_class/+room_bucket (+cache key).
  _STOCK_SQL и _SALES_WINDOW_SQL дедуплят до ПОСЛЕДНЕГО снапшота на физлот
  (DISTINCT ON project_name,corpus_name,section,floor,lot_number ORDER BY …
  snapshot_date DESC,id DESC), затем агрегируют. Class-фильтр (LOWER=LOWER,
  class lowercase) + room-bucket (Source-B room_area-вокабуляр, зеркало
  sales_series.room_area_bucket_of → what_to_build фильтрует без перевода).
  ROLLUP/GROUPING сохранён; confidence считается на дедуплицированных counts.
- demand_supply_forecast: base_pace и open-сток теперь ПОСЕГМЕНТНЫЕ
  (market_metrics(obj_class,room_bucket)). При заданном сегменте L2/L3
  (hidden/future) ИСКЛЮЧЕНЫ из баланса — они класс/формат-агностичны, иначе
  двоились бы по всем ячейкам. +_market_room_bucket VOCAB-мост (валидирующий
  pass-through Source-B меток; неизвестное → None = без фильтра, не тихий 0-rows).
- what_to_build/_DEFAULT_CLASSES и recommendation Economy-маппинг: «эконом»→
  «стандарт» (в objective_lots эконома НЕТ, стандарт=483k → раньше ячейка
  матчила 0 строк и молча выпадала).
- report_assembler honesty-guard: если ВСЯ сетка прижата к ±1.0
  (degenerate-fallback) — не эмитим «строить»/«избегать», показываем
  «недостаточно гранулярных данных для посегментного вывода».
- data/sql/173_objective_lots_physflat_idx.sql: partial index под DISTINCT ON
  (Index Only Scan + Unique, без Sort на 1.75M строк; idempotent, BEGIN/COMMIT).

Prod-verify (parcel 66:41:0205010:287, Железнодорожный, h=24): ячейки
ДИФФЕРЕНЦИРУЮТ (12 measured, 7 distinct) вместо all −1.0; MOI комфорт/студия
38.5 vs стандарт/студия 244.3 (точное совпадение с ожидаемым).

Тесты: регрессия «ячейки различаются (не all −1.0)» + vocab-translation +
honesty-guard + посегментное предложение. ruff clean; no :name::type.
2026-06-27 22:50:58 +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
23cc188972 fix(perf): move migration 171 to repo-root data/sql/ so the deploy applies it
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m52s
CI / backend-tests (pull_request) Successful in 12m0s
The objective_corpus_room_month index migration (#1942) was committed to
backend/data/sql/171_*, but the main deploy runner applies migrations from
repo-root data/sql/*.sql (deploy.yml:280) and the path trigger is data/sql/**.
So the file never ran — prod still seq-scans (verified post-deploy: index
absent, query 265ms). Move it to data/sql/171_* (alongside 170) so it deploys.
No SQL change; the index itself was dry-run-verified on prod (BEGIN/ROLLBACK).
2026-06-27 10:06:33 +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
9c52e0b29f feat(etl): housing-class normalization fallback via yandex_realty trigram match (#38) (#1911)
Some checks failed
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
housing-class normalization fallback via yandex_realty trigram (#38): migration 169 + ETL + COALESCE consumers. Refs #38
2026-06-26 07:44:25 +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
d46695ba50 fix(sql): ALTER TABLE IF EXISTS in migration 162 — district_economics absent on main DB (deploy red)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m24s
CI / backend-tests (pull_request) Successful in 9m4s
2026-06-17 20:02:06 +00:00
4ebc271a6c Merge pull request 'feat(site-finder): РИАСУРТ Свердл 14-layer harvest client+schema+task for ЕКБ agglomeration (#108)' (#1712) from feat/riasurt-sverdl-layers-108 into main
Some checks failed
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m27s
Deploy / build-worker (push) Successful in 3m20s
Deploy / deploy (push) Failing after 37s
2026-06-17 19:52:58 +00:00
0a72ef9491 merge main + renumber 163→167 + guard riasurt harvest (#108 review)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m33s
CI / backend-tests (pull_request) Successful in 10m10s
2026-06-17 22:52:15 +03:00
ddf4873874 fix(riasurt): renumber migration 163→167 + guard quarterly harvest behind flag (#108 review) 2026-06-17 22:45:23 +03:00
13db14c8c9 Merge pull request 'feat(objective): diff-gated objective_lots_history writer trigger (#307 OBJ-6)' (#1719) from feat/objective-lots-history-writer-307-obj6 into main
Some checks failed
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 31s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 37s
Deploy / deploy (push) Failing after 41s
2026-06-17 19:44:25 +00:00
e4cc2d9625 chore(sql): renumber objective history writer migration 164→166 (avoid collision)
Some checks failed
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 1m47s
CI / backend-tests (pull_request) Failing after 9m16s
2026-06-17 22:43:55 +03:00
81e708b953 Merge pull request 'feat(sql): sales-tracker velocity + absorption MVs for Site Finder (#61)' (#1711) from feat/velocity-mv-61 into main
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
2026-06-17 19:43:14 +00:00
2cf6261005 chore(sql): renumber sales-tracker MV migration 161→164 (avoid collision)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Failing after 1m49s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 1m39s
CI / backend-tests (push) Failing after 8m57s
CI / backend-tests (pull_request) Failing after 9m6s
2026-06-17 22:36:39 +03:00
24615b96c1 fix(site-finder): gate velocity on is_reviewed objective_complex_mapping (#307 OBJ-2)
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 2m23s
CI / backend-tests (pull_request) Failing after 9m12s
2026-06-17 22:32:52 +03:00
3a3f934bac feat(objective): diff-gated objective_lots_history writer trigger (#307 OBJ-6)
Some checks failed
CI / changes (pull_request) Successful in 9s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 2m24s
CI / backend-tests (pull_request) Failing after 9m11s
2026-06-17 22:31:51 +03:00
31e316e04e feat(site-finder): РИАСУРТ Свердл 14-layer harvest client+schema+task for ЕКБ agglomeration (#108)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
2026-06-17 21:49:04 +03:00
96a9c575b4 feat(sql): sales-tracker velocity + absorption MVs for Site Finder (#61)
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Failing after 1m43s
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / backend-tests (push) Failing after 8m50s
CI / backend-tests (pull_request) Failing after 8m43s
2026-06-17 21:48:11 +03:00
0fb70f2f90 Merge pull request 'fix(sql): EMISS PK includes period_type so yearly+Q1 coexist (#1606 follow-up)' (#1706) from fix/emiss-pk-granularity-1687 into main
Some checks failed
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m45s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
2026-06-17 18:46:00 +00:00
2659600374 fix(sql): emiss backfill period_type='quarter' to match scraper (#1606 review)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Failing after 1m37s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 1m35s
CI / backend-tests (push) Failing after 8m53s
CI / backend-tests (pull_request) Failing after 8m41s
2026-06-17 21:45:12 +03:00
5170107405 fix(sql): EMISS PK includes period_type so yearly+Q1 coexist (#1606 follow-up)
Some checks failed
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
Adds period_type TEXT NOT NULL DEFAULT 'unknown' to macro_indicator and
widens the PRIMARY KEY to (indicator_type, region, obs_date, period_type).

Before: yearly aggregate ('год'→obs_date YYYY-01-01) and Q1 ('I квартал'→
same date) shared the same PK slot → ON CONFLICT DO UPDATE made them
overwrite each other despite the in-memory dedup fix in #1687.

After: each EmissRow carries period_type from _emiss_period_granularity
('year'/'quarter'/'month'); DB ON CONFLICT targets include it, so
yearly+Q1 rows genuinely coexist.

Non-EMISS sources (CBR, rosstat open-data, domrf) use period_type='unknown'
(literal in SQL) — they are disambiguated by obs_date already, so the
wider PK is backward-compatible. All ON CONFLICT clauses in
cbr_macro_sync.py and rosstat_macro_sync.py updated accordingly.

Migration: 163_emiss_pk_period_type.sql (idempotent BEGIN/COMMIT).
Tests: 49 passed (emiss + rosstat suite), ruff clean, py_compile OK.
2026-06-17 21:27:34 +03:00
9e5086890f fix(site-finder): remove dead sat_factor (computed+written, never applied) (#1509)
Some checks failed
CI / changes (pull_request) Has been cancelled
CI / backend-tests (pull_request) Has been cancelled
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been cancelled
CI / frontend-tests (push) Has been cancelled
CI / openapi-codegen-check (push) Has been cancelled
sat_factor = 1 + ((sold_pct-50)/100)*0.30 was computed in 09_macro_and_trend.py,
written to district_economics.sat_factor, and fetched in server.py and 10_score_v2.py
— but never multiplied into any score. The live market sub-score uses a separate
sat_score = min(100, sold_pct*100/70) directly, so sat_factor was dead code that
would double-count absorption if ever wired in.

- 09_macro_and_trend.py: remove sat_factor computation, ALTER TABLE column, UPDATE
  binding, and debug print column
- 10_score_v2.py: remove sat_factor from SELECT and unpacking
- server.py: remove sat_factor variable assignment and from macro_factors response
- static/index.html: remove sat_factor documentation row
- data/sql/162_drop_district_economics_sat_factor.sql: DROP COLUMN IF EXISTS
2026-06-17 21:08:54 +03:00
7d8d12f5a6 Merge pull request 'fix(sql): v_supply_layers_latest keep dev_group_name in DISTINCT ON key (#1650)' (#1688) from fix/supply-view-devgroup-key-1650 into main
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
2026-06-17 17:55:31 +00:00
ba83c36bf4 Merge pull request 'fix(sql): dedup deals-to-district assignment in ekb districts median refresh (#1352)' (#1678) from fix/district-median-dedup-1352 into main
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
2026-06-17 17:45:31 +00:00
986514be3e fix(sql): v_supply_layers_latest keep dev_group_name in DISTINCT ON key (#1650)
Some checks failed
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m43s
CI / backend-tests (pull_request) Failing after 8m50s
2026-06-17 20:37:46 +03:00
04d1a64e25 fix(sql): dedup deals-to-district assignment in ekb districts median refresh (#1352)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m56s
CI / backend-tests (push) Failing after 9m1s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m44s
CI / backend-tests (pull_request) Failing after 9m12s
Replace ST_Intersects(district.geom, quarter.geom) with ST_Within(ST_Centroid(quarter.geom), district.geom)
so each cadastral quarter is assigned to exactly one district. Eliminates 85 double-counted quarter-district
pairs and 307 spurious deal-district rows that were skewing PERCENTILE_CONT medians. Function signature
unchanged; no backend code changes required.
2026-06-17 20:29:34 +03:00
13327e5a2c fix(cadastre): cad_territorial_zones MULTIPOLYGON typmod + ST_Multi coercion (#1344)
Some checks failed
CI / changes (push) Successful in 12s
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Successful in 2m12s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m42s
CI / backend-tests (push) Failing after 9m11s
CI / backend-tests (pull_request) Failing after 8m55s
2026-06-17 20:23:56 +03:00
c6bfce3d1a fix(migrations): живые миграции для прод-фиксов #1361/#1419/#1364
Some checks failed
CI / changes (push) Successful in 10s
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (push) Successful in 1m22s
CI / openapi-codegen-check (push) Failing after 2m22s
CI / frontend-tests (pull_request) Successful in 1m5s
CI / openapi-codegen-check (pull_request) Failing after 1m45s
CI / backend-tests (push) Failing after 9m8s
CI / backend-tests (pull_request) Failing after 8m54s
Правки уже-применённых миграций (85/062/080) не долетают до прода через
_schema_migrations. Дописаны отдельные миграции на живых таблицах:
- 158: pzz_zones_ekb UNIQUE NULLS NOT DISTINCT (rosreestr_id) (#1361)
- tradein 108: повторный backfill Avito-адресов улучшенным regexp (#1419)
- tradein 109: пересчёт asking_to_sold_ratios secondary-only (#1364/#1186)

Все три dry-run (BEGIN..ROLLBACK) против прод-схемы: PSQL_RC=0.
2026-06-15 21:31:28 +03:00
479f01ae1a Merge remote-tracking branch 'forgejo/main' into HEAD
Some checks failed
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (push) Successful in 1m7s
CI / openapi-codegen-check (push) Failing after 2m9s
CI / frontend-tests (pull_request) Successful in 1m4s
CI / openapi-codegen-check (pull_request) Failing after 1m37s
CI / backend-tests (pull_request) Failing after 9m7s
CI / backend-tests (push) Failing after 9m11s
# Conflicts:
#	backend/app/services/site_finder/gate_verdict.py
2026-06-15 21:07:05 +03:00
86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00
863577beb6 feat(sql): corroborated trgm backfill objective_complex_mapping +17 (#307 OBJ-2 r2) (#1333)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 27s
Deploy / build-worker (push) Successful in 27s
Deploy / deploy (push) Successful in 1m4s
2026-06-14 16:45:55 +00:00
5bf30570a5 perf(sql): geography GIST + covering index для analyze-пути Site Finder (157) (#1334)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
2026-06-14 16:45:22 +00:00
f9dd903f4d feat(sql): conservative trgm backfill objective_complex_mapping +23 (#307 OBJ-2) (#1331)
All checks were successful
Deploy / build-backend (push) Successful in 29s
Deploy / build-worker (push) Successful in 29s
Deploy / deploy (push) Successful in 1m1s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
2026-06-14 15:51:38 +00:00
72e0b050f9 feat(site-finder): POI saturation per capita по районам ЕКБ (#42) (#1329)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 2m52s
Deploy / deploy (push) Successful in 1m12s
2026-06-14 15:05:22 +00:00
11aa011d11 perf(db): covering-index objective_lots district-price (#70) + 152-ФЗ/perf audit docs (#70 #79)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 2m3s
CI / openapi-codegen-check (pull_request) Successful in 2m7s
CI / backend-tests (push) Successful in 8m55s
CI / backend-tests (pull_request) Successful in 8m58s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 29s
Deploy / build-worker (push) Successful in 29s
Deploy / deploy (push) Successful in 1m10s
#70: EXPLAIN top-5 hot-path на prod — analyze district-price block по objective_lots
(1.12M) делал Bitmap Heap Scan ~41k блоков (cold 5.3s/warm 474ms), пробивая p95 ≤3с.
Partial covering-index (district, price_per_m2_rub) WHERE price_per_m2_rub IS NOT NULL →
Index Only Scan (verified scratch: 87ms, Heap Fetches 0). Требует VACUUM ANALYZE
objective_lots post-deploy (stale VM иначе глушит выигрыш). database-expert  verified.

#79: 152-ФЗ PII grep-аудит — plain-PII в логах НЕ найдено; Sentry scrub / PRINZIP-хеширование
/ LLM §19-redaction подтверждены. Остаточный риск: pilot_requests хранит сырые ПДн без
retention (рекомендация в docs). httpx DEBUG off (ops-инвариант).

Closes #70
Closes #79
2026-06-14 00:09:09 +05:00
658d724075 feat(analytics): velocity-anomaly alerts (#17) + ARN ДДУ price indicator (#99)
Some checks failed
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 7m37s
CI / backend-tests (pull_request) Successful in 7m42s
Deploy / deploy (push) Blocked by required conditions
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#17: detect_velocity_anomalies + GET /analytics/velocity-alerts — z-score drop-detection
на domrf_kn_sale_graph (double-gate z≤-2 AND drop≤-30%, starvation-guards). Snapshot=richest
(не MAX — prod новейшие частичные), lookback anchored на latest report_month (scrape лаг ~4мес).
Prod: ЖК Центральный Парк -69%, ~14ms.

#99: mv_ddu_price_indicator (миграция 152) + POST /market/ddu-indicator — ARN-mirror
ценовой индекс per quarter×area_bucket из rosreestr_deals (ДДУ регион 66). MVP: subject-level,
period Q, window 2025-Q2+, methods 1/2 (basis/previous, prev_period_value honesty). Q1-2026
headline 1.0185 vs ARN 1.03 (±5%). Method 3 blocked (нет pre-2025-Q2 данных) — задокументировано.

Closes #17
Closes #99
2026-06-13 22:04:49 +05:00
1883271908 fix(cadastre): NSPD 500-retry + per-layer skip (#252) + disable dead pzz-beat (#259) + geom backfill (#200)
Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m43s
CI / backend-tests (pull_request) Successful in 6m41s
Deploy / build-backend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#252: NspdBulkServerError на HTTP 5xx + WMS <ServiceException>-body → quarter-level
autoretry; _grid_walk_category флагует layer_failed только если ВСЕ cells 5xx (none ok),
network-errors исключены — пустой/флапающий слой не ложно-скипается. harvest_meta.layer_X_failed.

#259: pzz-sync-monthly beat отключён (PKK6 deprecated, pzz_zones_ekb=0, заменён
zone_regulation_cache+NSPD dumps). logger.exception→warning (стоп GlitchTip BACKEND-1B),
raise сохранён для admin-endpoint.

#200: geom_unavailable флаг (миграция 151) + backfill_parcel_geom task — grid-walk
layer 36368 per quarter лечит 930/964 NULL-geom участков (NSPD search вернул центроиды),
остаток marks unavailable (не ретраить вечно). SAVEPOINT per-cell, idempotent.

Closes #252
Closes #259
Closes #200
2026-06-13 20:45:05 +05:00
d1bc59a5dd chore(objective): fuzzy backfill objective_complex_mapping +115 dev-corroborated (#307 OBJ-2)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m31s
CI / backend-tests (pull_request) Successful in 6m29s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m16s
Deploy / build-worker (push) Successful in 3m34s
Deploy / deploy (push) Successful in 1m28s
P0 goldmine: objective_lots (1.12M, 81% цен) связывается с domrf для velocity/
layout-аналитики. Mapping было 148 → +115 (107 high + 8 mid) = 263.

Fuzzy-match objective(project_name) ↔ domrf_kn_objects(comm_name) LATEST snapshot
region_cd=66 через pg_trgm на НОРМАЛИЗОВАННЫХ именах (strip ЖК-boilerplate с
\m\M word-boundary). Правило приёмки dev-corroborated: nsim≥0.62 AND dsim≥0.5 —
precision over recall для P0-цен (generic-имена с разным застройщиком, напр.
Солнечный/Прекрасный/Стрелки dsim=0.0, отклонены). match_method='auto_fuzzy_v2',
is_reviewed=false (для human-review топ-кандидатов), confidence-tier в note.
Idempotent ON CONFLICT (objective_complex_name, objective_group). REFRESH
CONCURRENTLY mv_layout_velocity после COMMIT.

Refs #307
2026-06-13 18:50:57 +05:00
b211183940 feat(site-finder): детерминированная атрибуция застройщика в analyze (#1088)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 8s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m33s
CI / backend-tests (pull_request) Successful in 6m33s
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m44s
Deploy / build-worker (push) Successful in 5m24s
Deploy / deploy (push) Successful in 1m30s
Сервис get_developer_attribution поверх fn_developer_for_parcel (миграция 149):
топ-1 застройщик участка + track-record (РНС/РВЭ + домрф) + nearby_developers.
Дедуп по норм-ИНН (лучший match_method первым). Wire в analyze additive без флага
(чистый DB-резолвер по индексам, graceful → None). Beat-refresh developer_registry
ежемесячно 05:30 МСК после ekburg-permits. Pydantic-схемы + 18 тестов.

Источники: ekburg_construction_permits (РНС, ИНН) ⋈ domrf_kn_objects по
нормализованному ИНН + spatial/quarter fallback. 68% domrf-застройщиков имеют РНС.
EXPLAIN: point-lookup 0.06ms, резолвер ~30ms (functional/GIST индексы в миграции).

Closes #1088
2026-06-13 16:13:45 +05:00
b85147a3a0 fix(db): dedupe domrf_kn_documents + UNIQUE NULLS NOT DISTINCT (#1206)
Some checks are pending
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
UNIQUE (obj_id, doc_type, doc_num, file_url) в м.112 без NULLS NOT
DISTINCT, doc_num nullable. В PG NULL != NULL в уникальном индексе →
для документов с docNum=null (документированный частый вход:
extract_documents:125-134 → doc_num=None) ON CONFLICT-арбитр в
scrapers/documents.py:59 НИКОГДА не матчился → каждый ежедневный sweep
domrf_kn.py:1903 молча вставлял новую строку-дубль вместо UPDATE.
Таблица росла ×N прогонов; дубли уходили в analytics_queries.py:1068.

Миграция 140:
1. Дедуп существующих NULL-дублей: ROW_NUMBER() PARTITION BY все 4 ключа
   ORDER BY scraped_at DESC, id DESC → оставляем самую свежую, удаляем
   остальные WHERE rn>1 AND doc_num IS NULL.
2. DROP старого autonamed constraint'а IF EXISTS.
3. ADD uq_domrf_kn_documents_logical UNIQUE NULLS NOT DISTINCT (...) с
   именованным guard по pg_constraint.conname (паттерн м.125 → idempotent).

Caller scrapers/documents.py НЕ меняется — после миграции ON CONFLICT-
арбитр начнёт матчить NULL-doc_num строки автоматически. Прецедент того
же класса: м.110 (uq_infra_dedupe), м.125 (uq_supply_layers_logical).

Closes #1206
2026-06-13 06:17:13 +00:00
df34e55ab4 feat(site-finder): own-portfolio data source for §25.3 cannibalization (#1169 PR1)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m25s
CI / backend-tests (pull_request) Successful in 6m22s
Deploy / build-frontend (push) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m31s
Deploy / build-worker (push) Successful in 2m39s
Deploy / deploy (push) Successful in 1m12s
Foundation PR: unified "our projects" source the §25.3 overlap engine (PR2)
will consume. Two origins normalized to OwnProject (class/timing/price/unit-mix):
- current <- domrf_kn_objects filtered by settings.own_developer_ids (numeric
  prefix of composite dev_id; empty -> [] graceful, no DB hit, no hardcoded id)
- future  <- new manual-entry own_planned_project entity (migration 148)

Adds OWN_DEVELOPER_IDS config (comma-sep -> list[int], default []),
own_planned_project table (range/unit_mix CHECKs via IMMUTABLE helper, generated
geom), /api/v1/own-projects CRUD (created_by from X-Authenticated-User), and
get_own_portfolio(db). Per-source graceful degradation; psycopg-v3 CAST clean.

Does not touch special_indices.py or parcels.py (out of scope).

Refs #1169
2026-06-08 16:21:53 +05:00
4b2e7d9af8 fix(db): sync location.demand_index comment to city-relative (#948)
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m26s
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 4m52s
Deploy / build-worker (push) Successful in 13m20s
Deploy / deploy (push) Successful in 1m18s
The demand_index DB column comment still said "насыщающее преобразование"
(saturating) after #1167 switched it to city-relative normalization —
exactly the misleading-comment class that hid the original bug. Migration
147 updates COMMENT ON COLUMN (idempotent, comment-only, no data DDL).

Refs #948
2026-06-08 14:04:51 +05:00
8da1c00138 feat(location): district-level Location entity + indices (#948 part B)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m11s
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m22s
Promote district to a first-class `location` entity (ТЗ §8.2), ADDITIVE — no
district->FK refactor. New `location` table keyed by district_name (joinable by
string), carrying 4 normalized [0,1] indices (NULL when no data, never 0):
- infra: _district_poi_score / _city_avg_poi_score (per-district POI aggregate)
- competition: market_metrics.overstock_index (available/stuck competing supply
  — orthogonal to demand; NOT sell-through, which is market-heat correlated w/ demand)
- demand: market_metrics.unit_velocity (saturating /50)
- future_supply: future_supply_pressure.index (passthrough, already 0..1)

- data/sql/146_location.sql: idempotent table + UNIQUE(district_name) + range
  CHECK + centroid GIST
- services/site_finder/locations.py: compute_location_indices (reuses forecast
  per-district fns) + refresh_locations (SAVEPOINT per-row, CAST, ON CONFLICT)
- workers/tasks/location_refresh.py + beat (Mon 07:00 MSK, after supply-layers)
- api/v1/locations.py: read-only GET list + GET by name (analyst+admin via rbac,
  frontend pilot-gated)
- tests: 34 (normalization 0..1/null, competition⊥demand orthogonality, idempotent
  upsert, read API list/by-name/404)

Part of #948 (Part A insight shipped #1164).
2026-06-08 13:28:19 +05:00
6400ebde24 feat(insights): manual-entry Insight entity CRUD + §19 audit (#948 part A)
All checks were successful
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 1m41s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m50s
Deploy / deploy (push) Successful in 1m22s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m32s
Insight = аналитик пишет free-form intel про локацию/участок с пометкой
«непублично» (is_confidential, §7.13/§8.9). CRUD под /api/v1/insights;
created_by из X-Authenticated-User (не из тела — спуфинг автора невозможен).
Зеркало custom_pois: raw-SQL service + Pydantic v2 schemas, sync def handlers
(threadpool, off event loop).

- data/sql/145_insight.sql: table insight (idempotent, geom GENERATED из lon/lat,
  индексы district/cad_num/is_confidential/created_by/GIST); аддитивно расширяет
  CHECK audit_log.action += insight_write (тот же constraint ck_audit_log_action,
  to_regclass-guarded, superset — не ломает #962-аудит; НЕ правит applied 144).
- audit_middleware.classify_path: method-aware insight_write для POST/PUT/DELETE
  /api/v1/insights (GET-чтения не аудируются); обратно совместимо с parcels.
- ACL: backend rbac_guard hard-блокирует только /api/v1/admin/*; pilot отсекается
  frontend RouteGuard + Caddy (как parcels/custom_pois). is_confidential — стораемая
  пометка без per-record backend-гейта (analyst видит, per #962-политике).
- tests: 28 insight (CRUD+filters+required+#261 commit) + 5 audit. 86 зелёных, ruff.

Part B (Location first-class, §8.2) — отдельный follow-up.

Refs #948.
2026-06-08 12:41:39 +05:00
86828d0388 feat(rbac): add analyst role + §19 audit-log middleware (#962)
Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy Trade-In / changes (push) Successful in 7s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 36s
Deploy / build-backend (push) Successful in 1m39s
Deploy / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 48s
Deploy / build-worker (push) Successful in 2m41s
Deploy / deploy (push) Failing after 4s
Deploy Trade-In / deploy (push) Successful in 47s
CI / backend-tests (push) Successful in 6m38s
CI / backend-tests (pull_request) Successful in 6m30s
EPIC18/§19. analyst sees everything (deals, insights, exports, site-finder,
analytics, concept) EXCEPT admin/data-management. Enforcement is backend-hard
(the existing rbac_guard already 403s any non-admin role on /api/v1/admin/*, so
adding the role auto-blocks it) + frontend (deny_paths via /me) + audit.

§19 audit: new best-effort HTTP middleware logs the sensitive actions
(analyze / forecast / forecast-export) to a new audit_log table after the
response. Audit failures never break or delay-fail the request (2-layer
try/except + finally close). Registered INNER to rbac_guard so only authorized
requests are audited (a 403 short-circuits before audit). classify_path matches
export before forecast (anchored).

- auth/roles.yaml: analyst role (paths /**, deny admin-mgmt) + analysttest QA user
- core/auth.py (+ tradein mirror): Role Literal += analyst
- core/audit_middleware.py (new) + main.py registration
- data/sql/144_audit_log.sql (idempotent; auto-applies)
- tests: analyst rbac (403 admin / 200 parcels) + 11 audit cases

No data-level ACL (analyst sees data per policy) -> no #948 dependency. No new
deps. parcels.py untouched. Real analyst logins still need adding to
caddy/users.caddy.snippet (devops).

Refs #962.
2026-06-08 12:16:19 +05:00
3921180d07 fix(sf): ST_MakeValid genplan geom + backfill 881 invalid polygons (#1157)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 5s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 17:59:04 +00:00
9736192359 feat(sf): подзоны ПАТ Кольцово → pat_subzones (#1150)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m25s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m26s
Deploy / deploy (push) Successful in 1m11s
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-07 14:43:07 +00:00
50fcba1ca0 feat(sf): ingest ОКН-объектов ЕКБ из АИС ЕГРКН (точки+категория) → okn_objects (#1141)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m23s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m21s
Deploy / deploy (push) Successful in 1m15s
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
2026-06-07 12:51:18 +00:00