fix(ptica): подпись сетевого обременения называет все виды, а не первый (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m6s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m6s
`cad_utility_label` брался от ПЕРВОГО overlap'а с распознанным
`network_kind`, а покрытие агрегировалось по ВСЕМУ bucket'у. Участок под
двумя видами охранных зон получал в отчёте конкретную причину, дающую
малую часть площади:
«Сетевое обременение (теплоснабжение) покрывает 74% участка»
← теплоснабжение даёт 10%, остальные 64% — инженерные коммуникации
Это не редкость. Прод 20.08.2026: 316 пересечений охранных зон РАЗНЫХ
видов, 155 зон вовлечено; самая частая пара — «тепловых сетей» ×
«инженерных коммуникаций» (296 из 316).
Теперь копятся ВСЕ различённые виды и называются через запятую;
множественное число, когда их больше одного. Покрытие — их объединение,
и подпись это отражает.
Заодно снята зависимость от порядка: подпись бралась от первого overlap'а,
а он определяется `ORDER BY reg_numb_border, id`, который к покрытию
отношения не имеет. Виды сортируются.
Проверка попутно опровергла ДОВОД пункта эпика. Пункт говорит о смешении
сетевых зон с keyword-совпадениями без network_kind. На проде таких ноль:
из 3493 строк cad_zouit — 103 СЗЗ-предупреждения, 1936 с распознанной
сетью, 1454 generic warning, и НИ ОДНОЙ «только по ключевому слову»
(classify_network_zone уже покрывает все встречающиеся шаблоны). Вывод
пункта — «конкретная причина приписывается чужой площади» — верен, но по
другой причине: смешиваются РАЗНЫЕ ВИДЫ СЕТЕЙ, а не сети с keyword'ами.
Двусторонне: против origin/main три теста красные, головной — с полной
строкой, которую увидел бы пользователь. Контроли зелёные с обеих сторон:
один вид сохраняет прежнюю формулировку в единственном числе, две зоны
одного вида не дают дубль в подписи, area-gate не тронут (тонкая полоса
остаётся warning).
pytest test_gate_verdict + services/site_finder — 727 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3ecd7cffc8
commit
b1dc5d5507
2 changed files with 145 additions and 8 deletions
|
|
@ -327,8 +327,15 @@ def compute_gate_verdict(
|
|||
sub17_overlaps: list[dict[str, Any]] = []
|
||||
# cad_zouit path: сетевое обременение + keyword-blocker (утилитарная охранная зона).
|
||||
cad_utility_overlaps: list[dict[str, Any]] = []
|
||||
# Подпись вида сети для cad-detail (первый встреченный network_kind).
|
||||
cad_utility_label: str | None = None
|
||||
# Подписи видов сетей для cad-detail. Копим ВСЕ различённые виды, а не первый
|
||||
# (#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 []:
|
||||
src = overlap.get("source", "nspd-quarter-dump")
|
||||
if src == "cad_zouit":
|
||||
|
|
@ -360,10 +367,10 @@ def compute_gate_verdict(
|
|||
):
|
||||
# Утилитарная охранная зона — копим для area-gate (см. ниже).
|
||||
cad_utility_overlaps.append(overlap)
|
||||
if cad_utility_label is None and net_kind is not None:
|
||||
cad_utility_label = overlap.get("network_kind_label") or network_kind_label(
|
||||
net_kind
|
||||
)
|
||||
if net_kind is not None:
|
||||
_lbl = overlap.get("network_kind_label") or network_kind_label(net_kind)
|
||||
if _lbl and _lbl not in cad_utility_labels:
|
||||
cad_utility_labels.append(_lbl)
|
||||
else:
|
||||
warnings.append(
|
||||
Warning(
|
||||
|
|
@ -438,14 +445,20 @@ def compute_gate_verdict(
|
|||
pct = _coverage_pct_label(coverage)
|
||||
# Код-различение сетевого обременения (#1070) vs общего охранного keyword-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 is_network:
|
||||
blockers.append(
|
||||
Blocker(
|
||||
code="ZOUIT_NETWORK_OBREMENENIE",
|
||||
detail=(
|
||||
f"Сетевое обременение ({cad_utility_label}) покрывает {pct}% "
|
||||
f"{_обременение} ({cad_utility_label}) покрывает {pct}% "
|
||||
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