fix(site-finder): разгейтить geoportal зон-резолвер от gappy NSPD-дампа — починить видимость финмодели (#1881) #1891

Merged
bot-backend merged 3 commits from fix/zoning-resolution-decouple into main 2026-06-23 23:55:53 +00:00
Collaborator

Проблема (прод-замер 2026-06-24, postgres-gendesign MCP)

financial_estimate фаерит ~0% (0/542 анализов analyze-1.0 за 90д). Резолв ПЗЗ-зоны на участок проваливается на 94%: max_far есть у 15/542, can_build_mkd=true у 4/542, пересечение = 0.

В analyze два резолвера зоны:

  1. _get_zoning (nspd_quarter_dumps, точный ST_Intersects) — терзоны не замощают квартал плотно, центроид участка падает в 45–86 м зазор между валидными зонами → nspd_zoning=None у 512/542.
  2. get_or_fetch_zone_regulationEKBGeoportalClient.zone_index_at (живой геопортал, cache-first) — РАБОТАЕТ, резолвит gap-участки (проверено: возвращает Ж-5/ПК-1/ЦС-3 там, где дамп пуст).

Но (2), дающий max_far, был загейчен за _nspd_zoning is not None (parcels.py:2074) — зазор дампа глушил рабочий резолвер. Плюс дамп кладёт кадастровый рег-номер в zone_code, а gate.is_residential_zone матчит → не срабатывал.

Фикс (PR-A)

  • Убран _nspd_zoning is not None из условия — geoportal-резолвер работает при centroid (region-guard: только КН 66:41, геопортал ЕКБ-only).
  • Когда дамп зону не дал → синтезируем минимальный nspd_zoning из geoportal: zone_code = индекс зоны ("Ж-2") → is_residential_zone срабатывает → can_build_mkd резолвится для Ж-* зон. Write-back в nspd_dump_data["nspd_zoning"] доходит до gate / финмоста / ответа (читают по ключу).
  • Когда дамп зону далzone_code не перетираем (raw_props.subcategory детектит жильё), только добавляем regulation-поля. Расширение на Ц-/ЦС-PR-B.

Геопортал — authoritative (point-in-zone по ПОЛНОМУ слою ПЗЗ); «зазор» только в нашем кэше-дампе. Hot-path-safe (try/except, timeout 3с, cache-first).

Ожидаемый эффект

Прод-замер до: 66:41:0303006:930 (Ж-5 gap) → zone_code/max_far/financial=None, can_build=False. После деплоя ожидаем Ж-5 / max_far=4 / can_build=True / financial=PRESENT (провалидирую на проде).

Test plan

  • +7 тестов: синтез при dump=None+geoportal; нет синтеза при обоих None; dump zone_code не перетёрт; geoportal падает → деградация; is_residential_zone Ж-2=True / ПК-1/ЦС-3=False. Кейс синтеза ассертит РЕАЛЬНЫЙ gate can_build_mkd=True.
  • 47 passed, 0 failed (вкл. pre-existing zoning+gate). ruff + mypy чисто.
  • Схема не менялась → api-types regen не нужен (openapi-codegen-check пройдёт).
  • code-review: 2× approve, 0 majors (adversarial gate-correctness lens прошёл все 5 проверок: write-back доходит, нет false-positive от authoritative-источника, region-guard, hot-path try/except).

⚠️ Известные follow-ups

  • PR-B: broaden is_residential_zone на Ц-/ЦС- МКД-зоны через zone_regulation_cache.main_vri (нужна ПЗЗ-валидация какие индексы разрешают МКД).
  • Latency: zone_index_at — живой WFS-вызов на каждый ЕКБ-analyze (~1-2с, был только у 6%). Приемлемо (gated 3с timeout); оптимизация (dump-buffer-first) — отдельно.

Refs #1881

🤖 Generated with Claude Code

## Проблема (прод-замер 2026-06-24, postgres-gendesign MCP) `financial_estimate` фаерит **~0%** (0/542 анализов analyze-1.0 за 90д). Резолв ПЗЗ-зоны на участок проваливается на **94%**: `max_far` есть у 15/542, `can_build_mkd=true` у 4/542, пересечение = 0. В analyze **два** резолвера зоны: 1. `_get_zoning` (`nspd_quarter_dumps`, точный `ST_Intersects`) — терзоны **не замощают квартал плотно**, центроид участка падает в **45–86 м зазор** между валидными зонами → `nspd_zoning=None` у 512/542. 2. `get_or_fetch_zone_regulation` → `EKBGeoportalClient.zone_index_at` (живой геопортал, cache-first) — **РАБОТАЕТ**, резолвит gap-участки (проверено: возвращает Ж-5/ПК-1/ЦС-3 там, где дамп пуст). Но (2), дающий `max_far`, был **загейчен** за `_nspd_zoning is not None` (`parcels.py:2074`) — зазор дампа глушил рабочий резолвер. Плюс дамп кладёт кадастровый рег-номер в `zone_code`, а `gate.is_residential_zone` матчит `^Ж` → не срабатывал. ## Фикс (PR-A) - Убран `_nspd_zoning is not None` из условия — geoportal-резолвер работает при centroid (region-guard: только КН `66:41`, геопортал ЕКБ-only). - Когда дамп зону не дал → **синтезируем** минимальный `nspd_zoning` из geoportal: `zone_code` = индекс зоны (`"Ж-2"`) → `is_residential_zone` `^Ж` срабатывает → `can_build_mkd` резолвится для Ж-* зон. Write-back в `nspd_dump_data["nspd_zoning"]` доходит до gate / финмоста / ответа (читают по ключу). - Когда дамп зону **дал** → `zone_code` не перетираем (raw_props.subcategory детектит жильё), только добавляем regulation-поля. Расширение на Ц-*/ЦС-* → **PR-B**. Геопортал — authoritative (point-in-zone по ПОЛНОМУ слою ПЗЗ); «зазор» только в нашем кэше-дампе. Hot-path-safe (try/except, timeout 3с, cache-first). ## Ожидаемый эффект Прод-замер до: `66:41:0303006:930` (Ж-5 gap) → `zone_code/max_far/financial=None, can_build=False`. После деплоя ожидаем **Ж-5 / max_far=4 / can_build=True / financial=PRESENT** (провалидирую на проде). ## Test plan - [x] +7 тестов: синтез при dump=None+geoportal; нет синтеза при обоих None; dump zone_code не перетёрт; geoportal падает → деградация; `is_residential_zone` Ж-2=True / ПК-1/ЦС-3=False. Кейс синтеза ассертит **РЕАЛЬНЫЙ** gate `can_build_mkd=True`. - [x] 47 passed, 0 failed (вкл. pre-existing zoning+gate). ruff + mypy чисто. - [x] Схема не менялась → api-types regen не нужен (openapi-codegen-check пройдёт). - [x] code-review: **2× approve, 0 majors** (adversarial gate-correctness lens прошёл все 5 проверок: write-back доходит, нет false-positive от authoritative-источника, region-guard, hot-path try/except). ## ⚠️ Известные follow-ups - **PR-B:** broaden `is_residential_zone` на Ц-*/ЦС-* МКД-зоны через `zone_regulation_cache.main_vri` (нужна ПЗЗ-валидация какие индексы разрешают МКД). - Latency: `zone_index_at` — живой WFS-вызов на каждый ЕКБ-analyze (~1-2с, был только у 6%). Приемлемо (gated 3с timeout); оптимизация (dump-buffer-first) — отдельно. Refs #1881 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-06-23 22:25:39 +00:00
fix(site-finder): разгейтить geoportal зон-резолвер от gappy NSPD-дампа
Some checks failed
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 1m51s
CI / backend-tests (pull_request) Failing after 11m59s
CI / frontend-tests (pull_request) Has been skipped
46f021e1c8
financial_estimate фаерил ~0% в проде (0/542 анализов, 90д): резолв ПЗЗ-зоны на
участок проваливался на 94%. Причина — в analyze ДВА резолвера зоны:
(1) get_quarter_dump_data/_get_zoning (nspd_quarter_dumps, точный ST_Intersects) —
терзоны НЕ замощают квартал плотно, центроид участка падает в 45-86м ЗАЗОР между
валидными зонами → nspd_zoning=None у 512/542; (2) get_or_fetch_zone_regulation →
EKBGeoportalClient.zone_index_at (живой геопортал, cache-first) — РАБОТАЕТ, резолвит
gap-участки. Но (2), дающий max_far, был ЗАГЕЙЧЕН за `_nspd_zoning is not None`, т.е.
зазор дампа глушил рабочий резолвер. Плюс: dump кладёт кадастровый рег-номер
("66:41-7.14") в zone_code, а gate.is_residential_zone матчит ^Ж → не срабатывал.

Фикс (PR-A): убран `_nspd_zoning is not None` из условия — резолвер геопортала
работает при наличии centroid (region-guard: только КН 66:41, геопортал ЕКБ-only).
Когда dump зону не дал — синтезируем минимальный nspd_zoning из geoportal:
zone_code = индекс зоны ("Ж-2") → is_residential_zone ^Ж срабатывает → can_build_mkd
резолвится для Ж-* зон. Запись обратно в nspd_dump_data["nspd_zoning"] доходит до
gate / финмоста / ответа (читают по ключу). Когда dump зону ДАЛ — zone_code не
перетираем (raw_props.subcategory детектит жильё), только добавляем regulation-поля.
Геопортал — authoritative источник (point-in-zone по ПОЛНОМУ слою ПЗЗ); «зазор»
только в нашем кэше-дампе, не в реальности. Хот-path-safe (try/except, timeout 3с,
cache-first). Расширение dump-резидентности на Ц-*/ЦС-* → PR-B.

Тесты: +7 (синтез при dump=None+geoportal; нет синтеза при обоих None; dump zone_code
не перетёрт; геопортал падает→деградация; is_residential_zone Ж-2=True, ПК-1/ЦС-3=False).
Кейс синтеза ассертит РЕАЛЬНЫЙ gate can_build_mkd=True. 47 passed (вкл. pre-existing
zoning+gate), ruff+mypy чисто. Схема не менялась (api-types regen не нужен).

Прод-замер: 66:41:0303006:930 (Ж-5 gap) сейчас zone_code/max_far/financial=None,
can_build=False → после деплоя ожидаем Ж-5/max_far=4/can_build=True/financial=PRESENT.

Refs #1881

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Light1YT added 1 commit 2026-06-23 22:59:37 +00:00
fix(gate): main_vri-authoritative детекция МКД (убрать Ж-1/Ж-2 ИЖС false-positive, включить Ц-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) Successful in 1m51s
CI / backend-tests (pull_request) Failing after 12m21s
de13bfc64c
PR-A синтезировал zone_code = geoportal-индекс ("Ж-2") → gate.is_residential_zone
матчил ^Ж и считал ЛЮБУЮ Ж-* зону жилой-МКД. Но по zone_regulation_cache.main_vri
(authoritative ВРИ): Ж-1/Ж-2 = только ИЖС (нет кодов МКД) → ^Ж давал FALSE POSITIVE
("можно МКД" на ИЖС-участке — реальный дефект для девелопера); Ц-2 = разрешает МКД
(коды 2.5/2.6) но без Ж-префикса → FALSE NEGATIVE.

Фикс: is_residential_zone получил приоритет-0 — при наличии main_vri решает по нему
авторитетно (mkd_permitted_from_vri: коды 2.1.1/2.5/2.6 = МКД, точное совпадение
токена, "2.50"/"2.59"/"12.0" не матчат), переопределяя ^Ж/subcategory/keyword.
Эвристики остаются ТОЛЬКО как fallback при main_vri=None (dump-путь без geoportal).
parcels.py прокидывает _regulation.main_vri в nspd_zoning на ОБОИХ путях (synth +
dump-augment) → dump-зоны (Ц-2 и пр.) тоже апгрейдятся на main_vri-детекцию.
Унифицирует PR-A+PR-B. Обратная совместимость: main_vri default None.

Проверено по зонам: Ж-3/4/5 permit_mkd=True, Ж-1/2=False(ИЖС), Ц-2=True,
Ц-1/3/4+ЦС-*=False.

Тесты: +22 (mkd_permitted_from_vri true/false/none + no-false-prefix; override
suppress ИЖС / enable Ц-2 / overrides subcategory; fallback при None unchanged;
synth ИЖС-кейс не даёт residential). Полный бэкенд локально: 3442 passed, 0 failed,
coverage 70.3% (gate 65%). ruff чисто. Схема не менялась. Code-review 2×approve 0 major.

Refs #1881 #1891

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Light1YT added 1 commit 2026-06-23 23:43:48 +00:00
test(analyze): авто-стаб geoportal зон-резолвера в api/v1 тестах (CI desync fix)
All checks were successful
CI / openapi-codegen-check (pull_request) Successful in 1m46s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 11m26s
2e53f0a1c4
PR-A сделал get_or_fetch_zone_regulation БЕЗУСЛОВНЫМ в analyze. Analyze-тесты с
позиционным DB-моком (_make_db_for_analyze + call_idx/responses[]) не мокали
резолвер → новый безусловный вызов делает db.execute (get_cached) и сдвигает
последовательность ответов мока → parcel_meta получает чужой ответ = None.
Локально проходило (geoportal недоступен с dev-машины → резолвер возвращал None,
без db-вызова), в CI падало (раннер на проде ДОСТАЁТ geoportal → зона резолвится →
get_cached → desync): FAILED test_parcel_meta_found_in_cad_parcels.

Fix: autouse-фикстура в tests/api/v1/conftest.py глушит резолвер в no-op (return
None) для всех v1-тестов — блок 9e-bis ничего не синтезирует и не трогает БД
(идентично до-PR-A). test_analyze_zoning_regulation.py переопределяет резолвер
per-test (patch поверх autouse), поэтому его проверки сохранены.

Verify: затронутые analyze-файлы + zoning-файл = 72 passed, 0 failed. ruff чисто.

Refs #1891
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bot-backend merged commit 41d97aeaf9 into main 2026-06-23 23:55:53 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1891
No description provided.