Original CTE (CROSS JOIN LATERAL similarity на 1056 × 320 unmapped × top-3)
завис на deploy таймауте (89M cost узнан через EXPLAIN database-expert worker).
GIST trgm index — load-bearing для downstream queries, оставлен.
Multi-feature INSERT (composite >= 0.75) уже частично применён inline
2026-05-17 (+13 мапingов: 129 → 142). Полный backfill будет в отдельной
миграции 117 с pre-materialized temp tables для avoidance of full LATERAL
scan.
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>