Add _extract_obj_class_from_ai() with regex patterns for Комфорт/Бизнес/Премиум/Элит/Стандарт.
_norm_object() uses it as fallback; objClass from API still takes priority if ever returned.
Add backfill SQL 105 to retroactively populate existing rows from raw JSON payloads.
Coverage: ~1806/4548 rows will be filled; remainder NULL is expected (Нежилое/no AI desc).
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
(Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
emoji star removed (ui-ux.md), empty-state text min 30→15
Audit 2026-05-17: objective_corpus_room_month содержал активные сделки
для Малевич (98/12m) и Ньютон парк (30/12m), но записей в
objective_complex_mapping не было → mv_layout_velocity пропускал оба
проекта в velocity calculations.
Добавлены маппинги:
- Малевич → domrf_obj_id 64701 (Строящиеся, 244кв, сдача 2027-09)
- Ньютон парк → domrf_obj_id 47390 (Сданные, 411кв, крупнейший corpus)
ON CONFLICT DO NOTHING — идемпотентно. REFRESH mv_layout_velocity
выполняется вручную post-deploy (runbooks/Refresh_MV_Layout_Velocity.md).
Closes (epic part) #271 item 4
Add SQL migration 100_user_weight_profiles_default_seed.sql with system
presets Эконом/Комфорт/Бизнес (user_id='__system__'). Migration is
idempotent via ON CONFLICT DO UPDATE.
Backend:
- weight_profiles.py: add SYSTEM_USER_ID constant + list_profiles_with_system()
- admin_weight_profiles.py: add include_system query param to GET list endpoint
Tests: 3 new tests covering include_system flag and service sentinel behaviour.
PR #222 deploy failed на 99_nspd_entities_denorm.sql:
ERROR: syntax error at or near "а"
LINE 2: '3-сегментный квартал последнего harvest'а. Используется...
Unescaped apostrophe в COMMENT string closes literal early.
Fix: harvest'а → harvest''а (SQL escape).
Других unescaped апострофов в SQL литералах нет (другие 3 — в SQL комментариях
'--' , безопасно).
PR #173 fixed cad_buildings_pkey but missed 5 other indexes. Same root cause:
PG does NOT auto-rename indexes on table rename → all 6 backing indexes
remain on cad_buildings_old_apr26 with original names.
Add 5 more `ALTER INDEX IF EXISTS ... RENAME TO ..._old_apr26_*` statements
for: geom_gist, quarter_idx, objdoc_idx, complex_idx, purpose_idx.
Migration 92 still NOT in _schema_migrations (4× rollback now). Next deploy
will re-apply cleanly.
Co-authored-by: lekss361 <claudestars@proton.me>
Migration 92_cad_bulk_layers.sql fails on deploy with:
ERROR: relation "cad_buildings_pkey" already exists
CONTEXT: ALTER INDEX cad_buildings_new_pkey RENAME TO cad_buildings_pkey
Root cause: K.1 renamed cad_buildings → cad_buildings_old_apr26, but PG
does NOT auto-rename the backing PK index on table rename. The legacy
cad_buildings_pkey index is still attached to cad_buildings_old_apr26.
K.3 then tries to rename cad_buildings_new_pkey to the same name → collision.
Fix: add an explicit `ALTER INDEX IF EXISTS cad_buildings_pkey RENAME TO
cad_buildings_old_apr26_pkey` BEFORE the new rename. Idempotent (IF EXISTS).
Migration 92 was rolled back in last 3 deploy attempts (not in
_schema_migrations), so this edit will re-apply cleanly on next deploy.
Co-authored-by: lekss361 <claudestars@proton.me>
* fix(frontend): apiFetchWithStatus body stream double-read crash
Per user report 2026-05-14: 'Failed to execute text on Response: body stream
already read' при analyze с non-JSON error response.
apiFetchWithStatus делал .json() (consumes stream), потом .text() в catch
(FAILS — stream already consumed). Fetch API: body — ReadableStream,
consumable один раз.
Fix: read text() once → JSON.parse on string. На fail (HTML / non-JSON)
оборачиваем в {detail: rawText}. Никаких double-reads.
Site-finder cad search больше не крашится на error pages.
* feat(infra): auto-apply data/sql/*.sql migrations in deploy pipeline (#150)
Закрывает root cause production 500 на /api/v1/admin/site-finder/weight-profiles
(а также 2 unapplied migrations: #89/#91).
## Why это нужно
После audit обнаружили что:
- backend/alembic/versions/ пустой (Alembic configured но не используется)
- deploy.yml не имеет migration step
- 47+ raw SQL files накопилось без auto-apply
- 3 файла unapplied: 87 (engineering), 89 (drop brin), 90 (weight profiles)
User report: GET weight-profiles → 500 потому что user_weight_profiles
table не существует на prod.
## Changes
### deploy.yml +39 lines
- Path trigger расширен на data/sql/*.sql
- New step 'Apply DB migrations' между compose pull и compose up -d:
- Idempotent CREATE TABLE _schema_migrations
- Loop по data/sql/*.sql sorted → skip applied → psql with ON_ERROR_STOP=on
- Record applied filename в tracking table
- Exit 1 на failure → containers НЕ обновляются (no partial state)
### NN collision fix
- 87_engineering_networks_191fz.sql → 91_engineering_networks_191fz.sql
(collision с 87_on_demand_indexes.sql, мой renumber 85→87 был неудачным)
### CLAUDE.md +32 lines
- Subsection 'Auto-apply' под Migration pattern
- Bootstrap procedure docs
- Idempotency requirement
- Failure recovery
### scripts/bootstrap_schema_migrations.sh +100 lines
- One-time seed _schema_migrations с 48 already-applied files
- 3 файла intentionally NOT seeded: 89/90/91 (auto-apply на first deploy)
- Usage docs + verification query
## Vault
domains/infra/Runbook_Auto_Apply_Migrations.md NEW
infra-MOC.md updated
## Required manual steps ДО merge
1. SSH gendesign (tunnel)
2. POSTGRES creds export
3. bash scripts/bootstrap_schema_migrations.sh — seed 48 already-applied files
4. Verify: SELECT COUNT(*) FROM _schema_migrations → 48
5. После merge → first deploy auto-apply #89/#90/#91 → 500 closes
Closes#150
---------
Co-authored-by: lekss361 <claudestars@proton.me>
Per code review audit (May 14): файл без transaction wrapper — backfill CTE
INSERTs + view recreations executed как individual auto-commit statements.
Mid-file crash → orphan rows в complexes/complex_sources с no rollback path.
## Fix
`BEGIN;` после header comments (line 31, перед section 1).
`COMMIT;` в конце файла после последнего COMMENT ON VIEW.
Wraps все 6 секций atomically:
1-2: CREATE TABLE + CREATE INDEX для complexes + complex_sources
3-5: 3 backfill CTE INSERTs с ON CONFLICT DO NOTHING
6: DROP VIEW IF EXISTS + CREATE VIEW + COMMENT для v_complex_full
## No CONCURRENTLY split needed
Проверил — все 10 indexes используют plain `CREATE INDEX IF NOT EXISTS` без
CONCURRENTLY keyword. Безопасно обернуть в одну tx.
## Idempotency preserved
Все existing guards остаются:
- CREATE TABLE/INDEX IF NOT EXISTS
- ON CONFLICT (source, source_id) DO NOTHING
- DROP VIEW IF EXISTS
Re-running file двукратно — safe.
## Vault
`fixes/Migration_73_Complexes_Atomic_May14.md` — created (status: resolved).
Co-authored-by: lekss361 <claudestars@proton.me>
Per code review audit (May 14): BRIN index на partitioned parent table dead
weight. PG16 НЕ propagates такие индексы на child partitions; pg_stat
показывает idx_scan=0 → никогда не использовался.
## Why dead
rosreestr_deals — yearly partitioned по period_start_date. Queries делают
partition pruning через partition key. BRIN index на parent существует, но:
1. На child partitions его нет (PG16 не reflects)
2. Parent сам не имеет данных (partitioned out)
3. Queries никогда не trigger BRIN scan
Maintenance overhead (storage, autoanalyze) без value.
## Migration
`data/sql/89_drop_dead_brin_rosreestr_deals.sql` — атомарный DROP с idempotent
`IF EXISTS`. Apply order: после 88_*.
## Cleanup
`data/sql/01_schema_rosreestr_deals.sql:65-67` — закомментировал оригинальное
CREATE с note: fresh DB bootstrap НЕ создаст index заново.
## Rollback path
Если в будущем нужен range scan optimization — re-create per-partition:
```sql
CREATE INDEX rosreestr_deals_2024q1_period_brin
ON rosreestr_deals_2024q1 USING BRIN (period_start_date);
-- per-partition × N partitions
```
Но обычно partition pruning достаточно. Измерь EXPLAIN перед re-creation.
## Vault
`fixes/Migration_Drop_Dead_Brin_May14.md` создан (status: ready_to_apply).
Apply на prod: `psql ... -f data/sql/89_drop_dead_brin_rosreestr_deals.sql`
после merge.
Co-authored-by: lekss361 <claudestars@proton.me>
* fix(sql): renumber engineering_networks 85→87, dedup classid, add SRID
Three critical fixes на untracked 85_engineering_networks_191fz.sql per
code review (May 14) — до первого применения на prod.
1. **NN collision fix**: renumber 85_engineering_networks_191fz.sql → 87_
85_pzz_zones_ekb.sql уже занимает 85_ prefix. При alphabetic deploy
order `85_e*` применился бы ДО `85_p*` — но 87_ ставит нас после
86_v_bucket_success_score.sql без зависимости на 85_pzz.
2. **Duplicate classid '907015504'**:
- Был дважды: 'Опора металлическая' + 'Опора железобетонная'.
- Второй ON CONFLICT DO UPDATE молча перетёр бы первый → NSI код
металлической опоры терялся бы.
- Fix: 907015504 остаётся за металлической (lower-numbered subtype
per Минстрой NSI source), железобетонная переезжает на 907015507
(заполняет gap 906→508). Помечено comment-меткой 'verify with
Минстрой NSI table' до prod deploy.
3. **Untyped geometry**:
- Было: `geom GEOMETRY` без SRID constraint → разрешало бы insert
mix MSK-66 (srid=100000) и WGS84 (srid=4326) → silent ST_DWithin
corruption.
- Fix: `geom GEOMETRY(Geometry, 4326)`. Capital G позволяет
mixed Point/LineString/Polygon, SRID 4326 жёстко фиксирует WGS84.
ETL loader должен делать ST_Transform(msk66_raw, 4326) перед
insert (см. backend/app/core/crs.py для CRS utility).
Idempotency: BEGIN...COMMIT, IF NOT EXISTS, ON CONFLICT DO UPDATE
паттерн — уже корректные в файле.
Closes Day-1 critical #2 from code review audit (May 14).
Pre-deploy: verify 907015507 classid assignment against Минстрой
official NSI table; reassign and re-run migration if needed.
* fix(sql): remove provisional 907015507, fix Фонати→Фонари typo
Addresses review blocker on PR #118:
- Provisional classid 907015507 ('Опора железобетонная (столб)') убран
из seed insert. Commented out с TODO(NSI-verify) — следующий шаг
верифицировать против официальной NSI-таблицы Минстроя ДО восстановления
строки. Без верификации ON CONFLICT DO UPDATE name перетёр бы корректное
значение в eng_ref_classid.
- Опечатка 'Фонати электрические' → 'Фонари электрические' (907015602).
- Sort: 904015504/505/506/508 теперь в численном порядке (после удаления
провизорной 507).
Live seed теперь покрывает 504 (металл столб), 505 (металл ферма),
506 (металл фермовый столб), 508 (железобет ферма). Строка 507
restored after Минстрой NSI verification (см. follow-up).
---------
Co-authored-by: lekss361 <claudestars@proton.me>
* feat(site-finder): auto-fetch cadastre geometry on-demand (#93)
Когда пользователь вводит cad-номер которого нет в БД (cad_quarters_geom /
cad_buildings / cad_parcels_geom), вместо 404 «Загрузи через NSPD geo» (dead-
end для non-admin) — теперь backend автоматически инициирует NSPD fetch,
frontend показывает loading state с polling /fetch-status каждые 2с.
Backend:
- Новый модуль app/services/site_finder/cadastre_fetch.py с helpers:
find_or_enqueue_fetch (atomic check + enqueue с дедупликацией по cad),
fetch_status (smart polling endpoint — отличает not_in_nspd от failed),
detect_thematic_id (3-сегм quarter / 4-сегм parcel / 5-сегм building),
validate_cad_format.
- Reuse: enqueue_geo_job + process_nspd_geo_job (workers/tasks/nspd_geo).
source_kind='auto_on_demand' отличает от bulk; rate_ms=200, priority=9.
- POST /parcels/{cad}/analyze graceful fallback: inline await до 25s
(fast path), затем 202 + job_id + eta_seconds для polling, либо
400/404/503 в зависимости от статуса (с Retry-After 60s на 503).
- GET /parcels/{cad}/fetch-status новый endpoint для polling.
Frontend:
- useSiteAnalysis расширен: fetchingState + cancel(). POST analyze + при 202
polling каждые 2с (max 60 итераций = 2 мин cap). status=ready → re-trigger
analyze; not_in_nspd/failed/invalid_format → typed errors.
- apiFetchWithStatus<T> + HTTPError в lib/api.ts — status-aware variant
для 200 vs 202.
- FetchingState.tsx: animated spinner, progress bar (linear до etaSeconds,
asymptote после), elapsed counter, cancel button. Светло-голубая
scheme отличается от обычного pending skeleton.
- site-finder/page.tsx: FetchingState когда fetchingState активен; обычный
pending skeleton — только при первичном analyze без 202.
Edge cases (per #93 acceptance):
- cad валидный в НСПД, fetch <25s → inline 200 OK
- cad валидный, fetch >25s → 202 + frontend polls → ready → analyze
- cad валидный, не в НСПД → 404 с понятным сообщением + формат hint
- cad invalid format → 400 + формат hint
- NSPD rate-limited / failed → 503 + Retry-After 60s
- Параллельные запросы на тот же cad → один job, оба клиента poll'ят (дедуп
через find_active_on_demand_job).
Closes#93.
* fix(site-finder): address PR #95 auto-review minor feedback
Backend (cadastre_fetch.py):
1. (race condition) Advisory lock `pg_try_advisory_xact_lock(hashtext(cad_num))`
обёрнут вокруг шагов "check active job → enqueue" в find_or_enqueue_fetch.
Lock transaction-scoped, released at COMMIT. Параллельные запросы на тот
же cad: первый получает lock и enqueue; второй lock=false → re-check
active job (уже виден после первого COMMIT) → возвращает тот же job_id.
Docstring обновлён, упоминание SELECT FOR UPDATE удалено.
Backend (parcels.py):
3. (threadpool exhaustion) _INLINE_FETCH_WAIT_S снижен 25 → 15s с подробным
комментарием: tradeoff про default Starlette anyio threadpool (40 slots)
и concurrent burst saturation. 15s баланс: НСПД avg 5-15s для quarter,
~70% fast path; остальные 30% получают 202 + polling без блока.
Data (87_on_demand_indexes.sql):
2. (missing index) New migration:
- `nspd_geo_targets_cad_num_idx ON nspd_geo_targets(cad_num)` — для
find_active/recent_completed_job (existing UNIQUE composite не покрывает
WHERE cad_num=:c).
- `nspd_geo_jobs_source_status_idx ON nspd_geo_jobs(source_kind, status)`
composite для filter auto_on_demand + queued/running.
Idempotent (CREATE INDEX IF NOT EXISTS), не блокер при текущем размере,
критично при росте on-demand traffic.
Frontend (useSiteAnalysis.ts):
4. (UI flicker) setFetchingState(null) перемещён ПОСЛЕ `await second
apiFetch`. Иначе между clear и resolve есть момент когда mutation
isPending=true + fetchingState=null → пустой экран ~1 RTT.
NOT addressed (rebuttal):
5. (Tailwind convention) — проверил: в проекте нет ни globals.css, ни
@tailwind directives. ВСЕ существующие site-finder components используют
inline styles (ConfidenceBadge, GeotechRiskBlock, ScoreBreakdownPanel etc).
Tailwind в deps но не wired up. Keep inline styles для consistency.
6. (animate-spin) — требует Tailwind globals (см. #5). `<style jsx>`
keyframes — built-in Next.js, работает.
Per auto-review on 2252236.
---------
Co-authored-by: lekss361 <claudestars@proton.me>
Объединяю несколько связанных изменений вокруг NSPD geo bulk-fetcher:
Adapt to rosreestr2coord v5 API:
- nspd_lite.fetch_via_rosreestr2coord: drop `delay` kwarg from Area()
(removed upstream in v5); keep it in our function signature for
backward-compat, comment why.
- nspd_geo worker: add explicit time.sleep(rate_ms/1000) after lib-branch
fetch — in v4 the library throttled internally via delay, in v5
rate limiting is the caller's job. Без этого получали Area.__init__()
unexpected kwarg `delay` на каждом target.
Drop use_rosreestr2coord switch:
- Removed urllib-vs-lib choice everywhere. We always use community
rosreestr2coord library — авторы регулярно обновляют WAF-tricks,
наш urllib-fetcher (fetch_geoportal) уже неактуален.
- admin_scrape.py: Pydantic schema, INSERT, SELECT, API response
cleaned of `use_rosreestr2coord`.
- nspd_geo.enqueue_geo_job: param dropped, INSERT shrunk.
- worker process loop: dropped `if use_lib:` branch + import of
fetch_geoportal.
- frontend/geo/page.tsx: removed checkbox + GeoJob.use_rosreestr2coord
field + POST body field.
DB column drop:
- data/sql/78_drop_use_rosreestr2coord.sql (NEW):
DROP COLUMN nspd_geo_jobs.use_rosreestr2coord + CREATE OR REPLACE
VIEW v_scrape_runs_unified (которая depended on the column).
- data/sql/77_nspd_geo_jobs.sql: cleaned historical DDL for fresh setups.
- Migration applied to prod (in-conversation via postgres MCP).
Frontend polish:
- Thematic ID changed from free-form number input to labeled select
(1=parcel / 2=quarter / 4=admin / 5=building / 7=zone / 15=complex).
- Auto-sync thematic_id from Job kind on change (override possible).
- ScrapeLogsPanel: extended union type with "nspd_geo" + fixed
/admin/scrape/geo to pass scraperType="nspd_geo" (was "nspd",
filtering empty legacy nspd_scrape_log table; real logs live in
nspd_geo_log via v_scrape_log_unified).
Verified: ruff ✓, tsc --noEmit ✓, migration ran (BEGIN..COMMIT clean).
Deploy order safe: prod column уже удалена → новый backend код, который
не INSERT'ит use_rosreestr2coord, совпадёт со схемой после deploy.
ОТКРЫТИЕ: stdlib urllib проходит WAF nspd.gov.ru (TLS-fingerprint stdlib
отличается от mainstream HTTP-clients). Это позволяет полностью убрать
Playwright + Chromium для NSPD-задач.
* nspd_lite.py: urllib + ssl._create_unverified_context(). 4 публичных
fetcher'а (geoportal/quarter/parcel/building) + fetch_via_rosreestr2coord
fallback на community-lib
* schema 77: nspd_geo_jobs (журнал) + nspd_geo_targets (cad-номера со
статусом pending/done/failed). Resume-state в БД.
* tasks/nspd_geo.py: Celery task process_nspd_geo_job(job_id). Heartbeat
каждые 5 items, WAF backoff 30s × 2^N (max 8 → paused). UPSERT в
cad_quarters_geom / cad_buildings → идемпотентно.
* worker_ready hook: stale jobs (>10min без heartbeat) автоматически
re-enqueue после redeploy. Никаких потерь прогресса.
* 4 admin endpoint + UI /admin/scrape/geo с формой запуска (manual_list /
rosreestr_pending для авто-наполнения из ДДУ-кварталов), progress-bars
и cancel/resume кнопками per job.
* settings: use_nspd_lite=True, nspd_lite_rate_ms=600.
* dep: rosreestr2coord>=4.0.0 (community lib для fallback).
TODO следующих шагов:
- smoke-test на проде через SSH (validation RU-IP не банит urllib)
- feature toggle USE_NSPD_LITE в scrape_nspd.py для переключения с старого
Playwright-пути
- docker lean image (-300 MB после убирания Chromium)
- расширение SCRAPE_NSPD_DEFAULT_REGIONS на 66,74,72,59 (Челябинск/Тюмень/Пермь)
Objective API (api.objctv.ru) интегрирован как новый source of truth для
per-flat данных по новостройкам УрФО. Заменяет промежуточный Anton-SQLite
(legacy bootstrap-ETL остался как fallback в свёрнутом блоке UI).
Schema (data/sql/68_v2 — applied на проде, 6 таблиц):
- objective_lots (UPSERT по lot_id; 303 677 rows)
- objective_corpus_room_month (long-формат месяц×корпус×room_bucket; 19 738 rows)
- objective_lots_history (append-only weekly snapshots для elasticity)
- objective_complex_mapping (Objective ComplexName ↔ domrf_kn_objects.obj_id)
- objective_raw_reports (jsonb страховка на смену схемы API)
- objective_scrape_runs (журнал прогонов)
+ data/sql/72_objective_sync_config (single-row динамический конфиг)
Backend:
- services/scrapers/objective.py: ObjectiveClient — Bearer-токен (Redis +
in-memory fallback), retry на 401/429/5xx, Retry-After header support
- services/objective_etl.py: ETL SQLite Антона → PG (legacy)
- services/objective_sync_config.py: read/update single-row config
- workers/tasks/scrape_objective.py:
* sync_objective_group: 2 рабочих отчёта (corp_sum, lots_pf), inline-парсинг
* sync_all_groups: wrapper, перебирает группы из БД-конфига с
inter-group паузой; PATCH-merge explicit args > DB config
- workers/tasks/objective_etl.py: Celery task для legacy bootstrap
- workers/celery_app.py: beat читает cron из БД при старте (fallback на env)
- api/v1/admin_scrape.py: 5 новых endpoints для /objective/*
Frontend (frontend/src/app/admin/scrape/objective/page.tsx):
- PRIMARY blue блок «🌐 Наш sync» с input для override групп
- Collapsible «⚙️ Настройки» с формой (cron + 8 параметров) → PUT в БД
- Coverage-панель с PG counts + строка про SQLite Антона как legacy
- Collapsible «🛠 Bootstrap ETL» — legacy-инструмент
Beat schedule: вторник 06:00 МСК, ~10-15 мин на 4 группы (Свердл.обл +
Челябинск + Тюмень + Пермь = ~700K квартир УрФО). Расписание и параметры
меняются через админку без редеплоя (cron требует restart beat).
Эмпирические находки об API (probe 2026-05-10):
- 13 из 21 проверенных group_name доступны на тарифе (включая «Свердловская
область», «Челябинск», «Тюмень», «Пермь», но НЕ «Свердловская обл» —
формат имени строгий)
- ComplexName требует БЕЗ префикса «ЖК» и БЕЗ кавычек
- Поле «Банк» для всех 303k = NULL (тариф не отдаёт), но «Тип обременения»
работает (36% строк = ипотека) → ipoteka_share возможен, банковская
атрибуция — нет
Docker:
- bind-mount /opt/gendesign/site-finder:/data/anton-sqlite:ro в worker
GitHub backlog: добавлены #22-25 (recommend_mix v3 — 4 сабтаска по
улучшению алгоритма на основе фидбэка про POI / границы районов /
конкурентов / окно данных / success-driven mix).
Knowledge graph (memory/memory-gendesign.jsonl): обновлены entities
Objective_Integration_May07_2026, Schema_Objective_v2_May07,
Objective_API_Findings_May07, Module_Objective_Client + новый
Session_End_May07_2026.
TODO для прода: прописать OBJECTIVE_API_KEY=<key> в backend/.env +
docker compose restart worker beat.
В nspd_kn.py:get_pending_cads и admin_scrape.py:nspd_coverage regex-фильтры
quarter_cad_number !~ '^00:00:' и !~ ':0000000$' падали с psycopg.errors.
DataError — двоеточие в text() трактуется как ':param'. Заменил на NOT LIKE
с bindparams (быстрее regex и без issue с парсингом).
Эффект на проде:
- /api/v1/admin/scrape/nspd/coverage возвращал 500 → теперь 200
- Кнопка «Запустить sweep» больше не дизейбленa (pending=964 теперь видно)
- Сам scraper тоже бы валился на этом же баге при beat-запуске
B1: дефолт months_window 12→24, расширение fallback 24→27 (max архив).
2024Q1+Q2 теперь реально влияют на recommend_mix (raw 784→3815 сделок
для Академического, ×4.9).
B2: per-bucket elasticity через MODE по семантике (is_studio+rooms),
а не PERCENTILE_CONT(0.5) по total_area. ЖК с mix studios+3к больше не
попадают в bucket "1-к" как среднее. Для Академического: 1-к получает
208 точек регрессии (vs 118), slope -1.17.
B3: share_window_months и elasticity_window_months в API scope —
прозрачность для UI tooltip про рассинхрон окон (24mo rosreestr vs
36mo sale_graph).
B4: cad_buildings.floors при INSERT больше НЕ приводится к INT —
NSPD отдаёт диапазоны "1-2", "2-3" которые теряются как NULL. Хранится
как TEXT, аналитика делает CAST через regex.
R1: data/sql/66_indexes_recommend.sql — 4 индекса для горячего пути
recommend_mix (sale_graph composite, kn_objects partial, kn_flats partial,
cad_buildings partial). Применены в проде через MCP.
R3: ON CONFLICT в insert_buildings теперь обновляет ВСЕ изменяемые
поля (purpose, address, floors, geom, registration_date) — раньше при
re-scrape апдейтились только cost/area/year, остальное игнорилось.
- 56_fetch_ekb_districts.py: Overpass query (admin_level=9 в bbox ЕКБ),
shapely-ассемблинг рваных OSM-веев в MultiPolygon, кэш в data/raw/.
- 56_schema_ekb_districts_geom.sql: CREATE TABLE с GIST.
- 57_kn_objects_district.sql: ALTER + backfill UPDATE через ST_Contains.
3032 ЖК Свердл проставили district_name (208 в Академическом, 158 в Чкаловском,
и т.д.), 454 за bbox ЕКБ остались NULL.
- domrf_kn.py: _backfill_district_names() после Phase A — каждый sweep
автоматически проставляет district_name свежезаписанным ЖК.
- analytics_queries.py: 4 места addr ILIKE → district_name = :dn
(_velocity_baseline, _elasticity_coef, _active_competitors_count, comparable).
- analytics_queries.py: graceful obj_class-degrade — если в районе все
obj_class=NULL (kn-API quirk), фильтр по target_class опускается на уровне
geo-запросов с warning. class_multiplier на ценах продолжает работать.
Smoke (Академический, Comfort, 50K м²): velocity_source=sale_graph (104 ЖК /
1872 точек), competitors=208 в районе, headline «3400 млн ₽ · 112 мес ·
ликвидность 21/100», 5 comparable ЖК района — все реалистичные.