fix(site-finder): NO_ENGINEERING_NEARBY is a warning, not a verdict blocker (#1617) #1697

Merged
lekss361 merged 1 commit from fix/gate-verdict-no-blocker-1617 into main 2026-06-17 18:11:45 +00:00
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_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:

View file

@ -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": "Жилая"}