fix(ptica): подпись сетевого обременения называет все виды, а не первый (#2464)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m49s
Deploy / build-worker (push) Successful in 6m39s
Deploy / deploy (push) Successful in 1m53s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 13s
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 2m49s
Deploy / build-worker (push) Successful in 6m39s
Deploy / deploy (push) Successful in 1m53s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 13s
Часть эпика #2464. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
c979cf886a
2 changed files with 145 additions and 8 deletions
|
|
@ -327,8 +327,15 @@ def compute_gate_verdict(
|
||||||
sub17_overlaps: list[dict[str, Any]] = []
|
sub17_overlaps: list[dict[str, Any]] = []
|
||||||
# cad_zouit path: сетевое обременение + keyword-blocker (утилитарная охранная зона).
|
# cad_zouit path: сетевое обременение + keyword-blocker (утилитарная охранная зона).
|
||||||
cad_utility_overlaps: list[dict[str, Any]] = []
|
cad_utility_overlaps: list[dict[str, Any]] = []
|
||||||
# Подпись вида сети для cad-detail (первый встреченный network_kind).
|
# Подписи видов сетей для cad-detail. Копим ВСЕ различённые виды, а не первый
|
||||||
cad_utility_label: str | None = None
|
# (#2464): покрытие ниже агрегируется по всем overlap'ам bucket'а, поэтому подпись
|
||||||
|
# от одного вида приписывала бы конкретную причину чужой площади. На проде
|
||||||
|
# 20.08.2026 это не редкость: 316 пересечений охранных зон РАЗНЫХ видов, 155
|
||||||
|
# зон вовлечено (чаще всего «тепловых сетей» × «инженерных коммуникаций»).
|
||||||
|
# Порядок в списке — по появлению, но наружу отдаём отсортированным: порядок
|
||||||
|
# overlap'ов задан `ORDER BY reg_numb_border, id`, а он к покрытию отношения
|
||||||
|
# не имеет, и делать подпись зависящей от него незачем.
|
||||||
|
cad_utility_labels: list[str] = []
|
||||||
for overlap in nspd_zouit_overlaps or []:
|
for overlap in nspd_zouit_overlaps or []:
|
||||||
src = overlap.get("source", "nspd-quarter-dump")
|
src = overlap.get("source", "nspd-quarter-dump")
|
||||||
if src == "cad_zouit":
|
if src == "cad_zouit":
|
||||||
|
|
@ -360,10 +367,10 @@ def compute_gate_verdict(
|
||||||
):
|
):
|
||||||
# Утилитарная охранная зона — копим для area-gate (см. ниже).
|
# Утилитарная охранная зона — копим для area-gate (см. ниже).
|
||||||
cad_utility_overlaps.append(overlap)
|
cad_utility_overlaps.append(overlap)
|
||||||
if cad_utility_label is None and net_kind is not None:
|
if net_kind is not None:
|
||||||
cad_utility_label = overlap.get("network_kind_label") or network_kind_label(
|
_lbl = overlap.get("network_kind_label") or network_kind_label(net_kind)
|
||||||
net_kind
|
if _lbl and _lbl not in cad_utility_labels:
|
||||||
)
|
cad_utility_labels.append(_lbl)
|
||||||
else:
|
else:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
Warning(
|
Warning(
|
||||||
|
|
@ -438,14 +445,20 @@ def compute_gate_verdict(
|
||||||
pct = _coverage_pct_label(coverage)
|
pct = _coverage_pct_label(coverage)
|
||||||
# Код-различение сетевого обременения (#1070) vs общего охранного keyword-blocker:
|
# Код-различение сетевого обременения (#1070) vs общего охранного keyword-blocker:
|
||||||
# blocker'у с network_kind отдаём ZOUIT_NETWORK_OBREMENENIE, иначе ZOUIT_CAD_BLOCKER.
|
# blocker'у с network_kind отдаём ZOUIT_NETWORK_OBREMENENIE, иначе ZOUIT_CAD_BLOCKER.
|
||||||
is_network = cad_utility_label is not None
|
is_network = bool(cad_utility_labels)
|
||||||
|
# Все различённые виды через запятую: покрытие — их объединение, и подпись
|
||||||
|
# обязана это отражать. Множественное число, когда видов больше одного.
|
||||||
|
cad_utility_label = ", ".join(sorted(cad_utility_labels))
|
||||||
|
_обременение = (
|
||||||
|
"Сетевые обременения" if len(cad_utility_labels) > 1 else "Сетевое обременение"
|
||||||
|
)
|
||||||
if coverage > threshold:
|
if coverage > threshold:
|
||||||
if is_network:
|
if is_network:
|
||||||
blockers.append(
|
blockers.append(
|
||||||
Blocker(
|
Blocker(
|
||||||
code="ZOUIT_NETWORK_OBREMENENIE",
|
code="ZOUIT_NETWORK_OBREMENENIE",
|
||||||
detail=(
|
detail=(
|
||||||
f"Сетевое обременение ({cad_utility_label}) покрывает {pct}% "
|
f"{_обременение} ({cad_utility_label}) покрывает {pct}% "
|
||||||
f"участка — застройка МКД невозможна"
|
f"участка — застройка МКД невозможна"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""Подпись сетевого обременения называет ВСЕ виды, а не первый попавшийся (#2464).
|
||||||
|
|
||||||
|
`cad_utility_label` брался от первого overlap'а с распознанным `network_kind`, а
|
||||||
|
покрытие агрегировалось по ВСЕМУ bucket'у. Участок под двумя видами охранных зон
|
||||||
|
получал в отчёте конкретную причину, дающую малую часть площади:
|
||||||
|
|
||||||
|
«Сетевое обременение (теплосеть) покрывает 74% участка»
|
||||||
|
← при том, что теплосеть даёт 10%, а остальные 64% — инженерные коммуникации.
|
||||||
|
|
||||||
|
Это не редкость. Прод 20.08.2026: **316 пересечений охранных зон РАЗНЫХ видов**,
|
||||||
|
155 зон вовлечено; самая частая пара — «тепловых сетей» × «инженерных коммуникаций»
|
||||||
|
(296 из 316).
|
||||||
|
|
||||||
|
Подпись, кроме того, зависела от порядка overlap'ов (`ORDER BY reg_numb_border, id`),
|
||||||
|
который к покрытию отношения не имеет. Теперь виды сортируются.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.services.site_finder.gate_verdict import compute_gate_verdict
|
||||||
|
|
||||||
|
_ЗОНА = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||||||
|
|
||||||
|
|
||||||
|
def _overlap(type_zone: str, coverage: float) -> dict:
|
||||||
|
return {
|
||||||
|
"source": "cad_zouit",
|
||||||
|
"type_zone": type_zone,
|
||||||
|
"layer": type_zone,
|
||||||
|
"name": type_zone,
|
||||||
|
"coverage_pct": coverage,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _блокер(verdict: dict, code: str) -> dict | None:
|
||||||
|
return next((b for b in verdict["blockers"] if b["code"] == code), None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_names_every_kind_that_feeds_the_coverage() -> None:
|
||||||
|
"""Головной: обе сети названы, потому что обе вошли в покрытие.
|
||||||
|
|
||||||
|
На origin/main в подписи стоит только «теплосеть», хотя 64 % площади дают
|
||||||
|
инженерные коммуникации.
|
||||||
|
"""
|
||||||
|
overlaps = [
|
||||||
|
_overlap("Охранная зона тепловых сетей", 0.10),
|
||||||
|
_overlap("Охранная зона инженерных коммуникаций", 0.64),
|
||||||
|
]
|
||||||
|
v = compute_gate_verdict(_ЗОНА, overlaps, [], {"available": True})
|
||||||
|
b = _блокер(v, "ZOUIT_NETWORK_OBREMENENIE")
|
||||||
|
assert b is not None, f"нет сетевого блокера: {v['blockers']}"
|
||||||
|
detail = b["detail"]
|
||||||
|
assert "инженер" in detail.lower(), f"вид, дающий большую часть покрытия, не назван: {detail!r}"
|
||||||
|
assert "тепл" in detail.lower(), f"второй вид пропал: {detail!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plural_wording_when_several_kinds() -> None:
|
||||||
|
"""Множественное число, когда обременений несколько — читается как факт, а не описка."""
|
||||||
|
overlaps = [
|
||||||
|
_overlap("Охранная зона тепловых сетей", 0.30),
|
||||||
|
_overlap("Охранная зона объектов электроэнергетики", 0.44),
|
||||||
|
]
|
||||||
|
v = compute_gate_verdict(_ЗОНА, overlaps, [], {"available": True})
|
||||||
|
b = _блокер(v, "ZOUIT_NETWORK_OBREMENENIE")
|
||||||
|
assert b is not None
|
||||||
|
assert b["detail"].startswith("Сетевые обременения"), b["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_kind_wording_unchanged() -> None:
|
||||||
|
"""Контроль от переусердствования: один вид — прежняя формулировка в единственном числе."""
|
||||||
|
v = compute_gate_verdict(
|
||||||
|
_ЗОНА,
|
||||||
|
[_overlap("Охранная зона трубопроводов (газопроводов)", 0.8)],
|
||||||
|
[],
|
||||||
|
{"available": True},
|
||||||
|
)
|
||||||
|
b = _блокер(v, "ZOUIT_NETWORK_OBREMENENIE")
|
||||||
|
assert b is not None
|
||||||
|
assert b["detail"].startswith("Сетевое обременение ("), b["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_label_is_independent_of_overlap_order() -> None:
|
||||||
|
"""Контроль детерминизма: порядок overlap'ов не должен менять подпись.
|
||||||
|
|
||||||
|
Он задаётся `ORDER BY reg_numb_border, id` — к покрытию отношения не имеет.
|
||||||
|
"""
|
||||||
|
a = _overlap("Охранная зона тепловых сетей", 0.30)
|
||||||
|
b_ = _overlap("Охранная зона инженерных коммуникаций", 0.44)
|
||||||
|
d1 = _блокер(
|
||||||
|
compute_gate_verdict(_ЗОНА, [a, b_], [], {"available": True}),
|
||||||
|
"ZOUIT_NETWORK_OBREMENENIE",
|
||||||
|
)
|
||||||
|
d2 = _блокер(
|
||||||
|
compute_gate_verdict(_ЗОНА, [b_, a], [], {"available": True}),
|
||||||
|
"ZOUIT_NETWORK_OBREMENENIE",
|
||||||
|
)
|
||||||
|
assert d1 is not None and d2 is not None
|
||||||
|
assert (
|
||||||
|
d1["detail"] == d2["detail"]
|
||||||
|
), f"подпись зависит от порядка:\n{d1['detail']}\n{d2['detail']}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_kind_named_once() -> None:
|
||||||
|
"""Контроль: две зоны ОДНОГО вида не дают дубль в подписи."""
|
||||||
|
overlaps = [
|
||||||
|
_overlap("Охранная зона тепловых сетей", 0.40),
|
||||||
|
_overlap("Охранная зона тепловых сетей", 0.35),
|
||||||
|
]
|
||||||
|
v = compute_gate_verdict(_ЗОНА, overlaps, [], {"available": True})
|
||||||
|
b = _блокер(v, "ZOUIT_NETWORK_OBREMENENIE")
|
||||||
|
assert b is not None
|
||||||
|
assert b["detail"].lower().count("тепл") == 1, b["detail"]
|
||||||
|
assert b["detail"].startswith("Сетевое обременение ("), b["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_thin_coverage_still_warning_not_blocker() -> None:
|
||||||
|
"""Контроль: правка подписи не трогает area-gate — тонкая полоса остаётся warning."""
|
||||||
|
v = compute_gate_verdict(
|
||||||
|
_ЗОНА,
|
||||||
|
[_overlap("Охранная зона тепловых сетей", 0.05)],
|
||||||
|
[{"name": "ТП-1"}],
|
||||||
|
{"available": True},
|
||||||
|
)
|
||||||
|
assert v["blockers"] == [], v["blockers"]
|
||||||
Loading…
Add table
Reference in a new issue