fix(site-finder): NO_ENGINEERING_NEARBY is a warning, not a verdict blocker (#1617)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / backend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (push) Failing after 1m40s
CI / frontend-tests (pull_request) Has been cancelled
CI / openapi-codegen-check (pull_request) Has been cancelled
CI / backend-tests (push) Failing after 8m47s

Introduce INFORMATIONAL_WARNING_CODES frozenset; verdict assembly now only
downgrades to «С ограничениями» when verdict-relevant warnings exist.
NO_ENGINEERING_NEARBY still appears in warnings[] for display but a clean
residential parcel (Ж-zone, no ЗОУИТ) correctly receives verdict_label=«Можно».
This commit is contained in:
bot-backend 2026-06-17 20:59:32 +03:00
parent 9a0ca9336f
commit ac19ef4823
2 changed files with 46 additions and 4 deletions

View file

@ -63,6 +63,11 @@ WARNING_TYPE_ZONE_KEYWORDS: tuple[str, ...] = ("сзз", "санитарно-з
# Engineering nearby search radius (метры) — совпадает с quarter_dump_lookup.py # Engineering nearby search radius (метры) — совпадает с quarter_dump_lookup.py
ENGINEERING_NEARBY_THRESHOLD_M = 200 ENGINEERING_NEARBY_THRESHOLD_M = 200
# Warning codes that are purely informational — they appear in warnings[] for the user
# but do NOT downgrade the final verdict from «Можно» to «С ограничениями» (#1617).
# A clean residential parcel with only these warnings still gets verdict_label="Можно".
INFORMATIONAL_WARNING_CODES: frozenset[str] = frozenset({"NO_ENGINEERING_NEARBY"})
# ── TypedDicts ──────────────────────────────────────────────────────────────── # ── TypedDicts ────────────────────────────────────────────────────────────────
@ -308,12 +313,17 @@ def compute_gate_verdict(
) )
) )
# Final verdict # Final verdict.
# Informational warnings (e.g. NO_ENGINEERING_NEARBY) appear in warnings[] for display
# but do NOT by themselves downgrade a clean parcel to «С ограничениями» (#1617).
verdict_relevant_warnings = [
w for w in warnings if w["code"] not in INFORMATIONAL_WARNING_CODES
]
can_build: bool | Literal["unknown"] can_build: bool | Literal["unknown"]
if blockers: if blockers:
can_build = False can_build = False
label = "Нельзя" label = "Нельзя"
elif warnings: elif verdict_relevant_warnings:
can_build = True can_build = True
label = "С ограничениями" label = "С ограничениями"
else: else:

View file

@ -88,14 +88,46 @@ def test_zouit_non_blocker_subcategory_is_warning():
def test_no_engineering_warning_not_blocker(): def test_no_engineering_warning_not_blocker():
"""Отсутствие инженерных сетей в 200м — предупреждение, не блокер.""" """NO_ENGINEERING_NEARBY — информационное предупреждение, не блокер и не даунгрейдит вердикт.
Чистый жилой участок (Ж-зона, нет ЗОУИТ) без сетей в 200м должен оставаться «Можно»
отсутствие сетей не является юридическим запретом на МКД (#1617).
"""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"} nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True}) verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
assert verdict["can_build_mkd"] is True # warnings не блокируют assert verdict["can_build_mkd"] is True
assert verdict["verdict_label"] == "Можно" # не «С ограничениями» (#1617)
assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"]) assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"])
assert verdict["blockers"] == [] assert verdict["blockers"] == []
def test_no_engineering_plus_real_warning_still_downgrades():
"""NO_ENGINEERING_NEARBY совместно с реальным предупреждением (ЗОУИТ) → «С ограничениями».
Реальный ЗОУИТ-warning должен продолжать даунгрейдить вердикт;
NO_ENGINEERING_NEARBY при этом тоже присутствует в warnings[] (#1617).
"""
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
overlaps = [{"subcategory": 5, "name": "Придорожная полоса", "layer": "12345"}]
# нет инженерных сетей рядом — пустой список
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is True
assert verdict["verdict_label"] == "С ограничениями"
assert any(w["code"] == "ZOUIT_SUB5" for w in verdict["warnings"])
assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"])
assert verdict["blockers"] == []
def test_no_engineering_real_blocker_still_rejects():
"""Реальный блокер (охранная зона ЛЭП) при отсутствии сетей → «Нельзя» (#1617)."""
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
overlaps = [{"subcategory": 17, "name": "ЛЭП 110кВ", "layer": "37578"}]
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
assert verdict["can_build_mkd"] is False
assert verdict["verdict_label"] == "Нельзя"
assert any(b["code"] == "ZOUIT_OVERLAP_SUB17" for b in verdict["blockers"])
def test_stale_dump_marks_source_partial(): def test_stale_dump_marks_source_partial():
"""stale=True → source='nspd_dump_partial'.""" """stale=True → source='nspd_dump_partial'."""
nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"} nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}