From ac19ef48232bceb4f8921049b607ea35007f4ba5 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 17 Jun 2026 20:59:32 +0300 Subject: [PATCH] fix(site-finder): NO_ENGINEERING_NEARBY is a warning, not a verdict blocker (#1617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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=«Можно». --- .../app/services/site_finder/gate_verdict.py | 14 ++++++-- backend/tests/test_gate_verdict.py | 36 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/backend/app/services/site_finder/gate_verdict.py b/backend/app/services/site_finder/gate_verdict.py index b860a46c..6981b992 100644 --- a/backend/app/services/site_finder/gate_verdict.py +++ b/backend/app/services/site_finder/gate_verdict.py @@ -63,6 +63,11 @@ WARNING_TYPE_ZONE_KEYWORDS: tuple[str, ...] = ("сзз", "санитарно-з # Engineering nearby search radius (метры) — совпадает с quarter_dump_lookup.py 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 ──────────────────────────────────────────────────────────────── @@ -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"] if blockers: can_build = False label = "Нельзя" - elif warnings: + elif verdict_relevant_warnings: can_build = True label = "С ограничениями" else: diff --git a/backend/tests/test_gate_verdict.py b/backend/tests/test_gate_verdict.py index 4378d5ab..f4073a9a 100644 --- a/backend/tests/test_gate_verdict.py +++ b/backend/tests/test_gate_verdict.py @@ -88,14 +88,46 @@ def test_zouit_non_blocker_subcategory_is_warning(): def test_no_engineering_warning_not_blocker(): - """Отсутствие инженерных сетей в 200м — предупреждение, не блокер.""" + """NO_ENGINEERING_NEARBY — информационное предупреждение, не блокер и не даунгрейдит вердикт. + + Чистый жилой участок (Ж-зона, нет ЗОУИТ) без сетей в 200м должен оставаться «Можно» + — отсутствие сетей не является юридическим запретом на МКД (#1617). + """ nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"} 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 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(): """stale=True → source='nspd_dump_partial'.""" nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}