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).
#1954 — площадь «—»: COALESCE(land_record_area, specified_area, declared_area)
в EGRN-блоке analyze (land_record_area NULL у 12809/42233 участков, но
specified_area заполнена). area_m2 для 66:41:0205010:287 теперь 106378 (был NULL).
#1954 — «Обновлено» сломано: cost_registration_date — мёртвая колонка (0/42233);
репойнт на updated_at (42233/42233 заполнено). Ключ ответа last_egrn_update_date
не меняется (additive value fix).
#1958 — confidence-фактор «Прогноз спрос/предложение» дублировался ×4
(по фактору на горизонт). Сворачиваем в один weakest-link'ом (MIN ранга
по горизонтам) в _component_confidences до confidence-движка (#990).
#1957 — ЗОУИТ backend:
- _get_cad_zouit_overlaps: DISTINCT ON (reg_numb_border) — дедуп дубль-строк
(одна физ. зона 2× с разным category_name). group_key cad_zouit→protected.
- _get_zouit_overlaps (dump path): subcategory→RU-тип карта (26→СЗЗ и др.,
коды сверены кросс-джойном dump↔cad_zouit на проде); type_zone из карты,
reg из props.options.reg_numb_border. Раньше отдавал blank-строки.
- унификация group_key (protected/engineering/okn/natural/other) + top-level
reg_numb_border в обоих путях.
UP038-модернизация isinstance в report_assembler (pre-commit ruff 0.7.4).
Frontend note (#1957): nspd_zouit_overlaps теперь всегда group_key из набора
{protected,engineering,okn,natural,other} — сырой 'cad_zouit' больше не отдаётся;
оба пути несут type_zone + reg_numb_border.
Tests: +4 _component_confidences collapse, +6 ЗОУИТ (subcategory map, dump
typing, DISTINCT ON), schema-test обновлён на protected. 451 passed targeted.
ZOUIT_OVERLAP_SUB17 (единственный subcategory-blocker = охранная зона
ЛЭП/газа/трубопровода) был ХАРД-блокером на ЛЮБОЕ пересечение → блокировал
434/574 (75.6%) участков как «нельзя строить МКД». Прод-замер: реальное
покрытие участка этими зонами median=6%, p90=47%, только 1/17 >60% — тонкие
инж-коридоры. По РФ охранная зона ограничивает застройку ВНУТРИ полосы, а не
весь участок (МКД сажается на незанятой части). Хард-блок на 6%-покрытии неверен
и (после фикса резолва зон #1891) прятал financial_estimate у ~90% участков.
Фикс: area-gate.
- quarter_dump_lookup._get_zouit_overlaps + _get_cad_zouit_overlaps: добавлен
coverage_pct (доля площади участка под зоной, в geography м², NULLIF-guard).
- gate_verdict.compute_gate_verdict: sub-17 (и cad network/blocker) АГРЕГИРУЮТСЯ
(sum-capped, консервативно к блоку) → блокер ТОЛЬКО если покрытие >
settings.gate_zouit_engineering_blocker_min_coverage (0.6, env-tunable), иначе
WARNING ZOUIT_ENGINEERING_PARTIAL с % покрытия. Больше нет 4× дубль-блокеров.
СЗЗ/OKN/прочие ЗОУИТ — без изменений (warnings). main_vri/резидентность — без
изменений.
- coverage_pct отсутствует (legacy/synthetic) → трактуем как 0 → warning
(документировано: дамп всегда даёт coverage; ложный хард-блок дороже).
Тесты: +помесь gate/aggregation/coverage (131 combined passed); правка
_make_zouit_row (7-я колонка coverage_pct) в tests/test_quarter_dump_lookup.py.
Полный бэкенд локально: 2 failed → fixed → перепроверка. ruff чисто. Схема не
менялась (gate_verdict/overlaps — dict[str,Any], api-types regen не нужен).
Refs #1881
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_get_zoning() read non-existent props (top-level reg_numb_border / zone_code /
type_zone / name) so nspd_zoning.zone_code & zone_name were NULL on every parcel.
Real dump props (verified prod, 796 features) carry only the registration number,
duplicated across descr / label / externalKey / options.reg_numb_border (no human
zone name exists in the NSPD dump). Map both fields to that reg-number with safe
nested narrowing (isinstance options dict) + legacy fallbacks; honest — no
fabricated name. Surfacing reg_numb_border also unblocks the КСИТ crosswalk (#1843).
Regression tests use the real prop shape (full / nested-only / opaque→None).
parcel_ird_overlaps ловил только (OperationalError, ProgrammingError).
При D9b-wiring в analyze malformed WKT в ST_GeomFromText давал PostGIS
ERROR → SQLAlchemy DataError, который пробивал try/except → analyze
падал вместо graceful-degrade.
- ird_overlay_lookup: + DataError в imports и в except tuple.
- quarter_dump_lookup: симметричное расширение в _get_engineering_*.
- test_ird_overlay_lookup: parametrized test_graceful_when_db_fails
покрывает 3 класса (Operational/Programming + DataError для WKT).
Closes#1095
Backend (quarter_dump_lookup.py):
- _acquire_harvest_lock: Redis SETNX TTL=120s на quarter, защищает от burst
N concurrent analyze, ставящих N одинаковых harvest task в очередь
- _trigger_harvest: использует lock перед apply_async, возвращает False если
lock уже взят (другой запрос триггернул раньше)
- make_empty_result/EMPTY_DUMP_RESULT: новое поле harvest_eta_seconds в
nspd_dump dict, типичный harvest_quarter = 60с
- /analyze: пробрасывает поле через nspd_dump dict (нет typed schema —
response_model=None для /analyze endpoint, dict уходит как есть)
Frontend (NspdFreshnessBadge, NspdZoningBlock):
- Countdown «НСПД: загрузка ~Nс» вместо бесконечного спиннера
- После остановки countdown (remaining=0) NspdZoningBlock показывает
«загрузка дольше обычного» + ссылку на ПКК вместо infinite skeleton
Tests: 5 новых unit + 2 для empty_result schema (всего +7, pass)
Closes#234 (UX-side; data-side resolves когда Sub-PR B + D merged).
TASK A (#29 G2): add parcel_meta to analyze response
- New ParcelMeta Pydantic schema in app/schemas/parcel.py
- SELECT from cad_parcels WHERE cad_num=:c in analyze_parcel() (step 9f)
- Returns permitted_use_established_by_document, land_record_category_type,
land_record_subtype, cost_value; None when row absent
- Tests: test_analyze_parcel_meta.py (found + not-found cases)
TASK B (#232 G3): cad_zouit fallback in _get_zouit_overlaps
- When nspd_quarter_dumps has zouit_count==0, fall back to ST_Intersects
query on cad_zouit (3483 rows, GIST indexed)
- Overlaps tagged with source='cad_zouit'; format compatible with NSPD path
- gate_verdict.py: BLOCKER_TYPE_ZONE_KEYWORDS tuple for keyword-based
classification (охранная зона / трубопровод / электр / газ -> blocker;
СЗЗ -> warning); NSPD subcategory path preserved backward-compat
- Tests: 6 new test cases in test_gate_verdict.py covering cad_zouit path
and backward-compat for NSPD subcategory path
Updated db.execute call sequence in test_analyze_*.py (index shift +1 at pos 10).