fix(ptica): «геологический риск» больше не выводится из шума (#2934) #3006
2 changed files with 125 additions and 13 deletions
|
|
@ -3156,11 +3156,23 @@ def analyze_parcel(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("district_price_block query failed for %s: %s", cad_num, e)
|
logger.warning("district_price_block query failed for %s: %s", cad_num, e)
|
||||||
|
|
||||||
# B5-6) Risk indicators — flood_zone из cad_risk_zones + noise_score + geology proxy (SF-B5)
|
# B5-6) Risk indicators — flood_zone + noise_score (SF-B5).
|
||||||
|
#
|
||||||
|
# `geology_risk_label` УБРАН (#2934). Он назывался геологическим риском, а
|
||||||
|
# вычислялся из подтопления и ШУМА: «high» при подтоплении, «medium» при шуме
|
||||||
|
# ≥65 дБ, иначе «low». Геологии в нём не было ни одного бита. При этом
|
||||||
|
# `cad_risk_zones` пуста (0 строк, писателя нет — см. #2934 п.6), поэтому
|
||||||
|
# подтопление приходило только из OSM-прокси «река ближе 200 м», и на
|
||||||
|
# тихом участке без реки поле всегда говорило «low» — зелёный вердикт,
|
||||||
|
# ни разу не подкреплённый проверкой геологии.
|
||||||
|
#
|
||||||
|
# Замена не нужна: соседний блок `geology` честно отдаёт
|
||||||
|
# `data_available: false`, когда данных нет. Потребителей у поля не было —
|
||||||
|
# ни фронт, ни экспортёры, ни §19-allowlist чата его не читали, а в схеме
|
||||||
|
# `risks: dict[str, Any]`, поэтому OpenAPI не меняется.
|
||||||
risks_block: dict[str, Any] = {
|
risks_block: dict[str, Any] = {
|
||||||
"flood_zone": False,
|
"flood_zone": False,
|
||||||
"noise_score": round(noise_score, 2),
|
"noise_score": round(noise_score, 2),
|
||||||
"geology_risk_label": None,
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with db.begin_nested():
|
with db.begin_nested():
|
||||||
|
|
@ -3182,20 +3194,13 @@ def analyze_parcel(
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
_flood = bool(flood_row and int(flood_row["cnt"]) > 0)
|
_flood = bool(flood_row and int(flood_row["cnt"]) > 0)
|
||||||
# Geology proxy через hydrology flood_risk_flag (уже посчитан выше)
|
# OSM-прокси «река или канал ближе 200 м» (посчитан выше в hydrology).
|
||||||
|
# На сегодня это ЕДИНСТВЕННЫЙ работающий источник этого признака:
|
||||||
|
# cad_risk_zones пуста, поэтому _flood всегда False (#2934 п.6).
|
||||||
_geo_flood = hydrology.get("flood_risk_flag", False) if hydrology else False
|
_geo_flood = hydrology.get("flood_risk_flag", False) if hydrology else False
|
||||||
_has_flood = _flood or _geo_flood
|
|
||||||
# geology_risk_label: high если flooding, medium если шум > 65дБ, иначе low
|
|
||||||
if _has_flood:
|
|
||||||
_geo_label: str | None = "high"
|
|
||||||
elif noise_db_max >= 65.0:
|
|
||||||
_geo_label = "medium"
|
|
||||||
else:
|
|
||||||
_geo_label = "low"
|
|
||||||
risks_block = {
|
risks_block = {
|
||||||
"flood_zone": _has_flood,
|
"flood_zone": _flood or _geo_flood,
|
||||||
"noise_score": round(noise_score, 2),
|
"noise_score": round(noise_score, 2),
|
||||||
"geology_risk_label": _geo_label,
|
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("risks_block query failed for %s: %s", cad_num, e)
|
logger.warning("risks_block query failed for %s: %s", cad_num, e)
|
||||||
|
|
|
||||||
107
backend/tests/api/v1/test_2934_no_fake_geology_label.py
Normal file
107
backend/tests/api/v1/test_2934_no_fake_geology_label.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""«Геологический риск» не выводится из шума (#2934).
|
||||||
|
|
||||||
|
`risks.geology_risk_label` назывался геологическим риском, а вычислялся так:
|
||||||
|
|
||||||
|
high — если подтопление
|
||||||
|
medium — если шум ≥ 65 дБ
|
||||||
|
low — иначе
|
||||||
|
|
||||||
|
Геологии в нём не было ни одного бита. При этом `cad_risk_zones` пуста (0 строк,
|
||||||
|
писателя нет), поэтому подтопление приходило только из OSM-прокси «река ближе
|
||||||
|
200 м» — и на тихом участке без реки поле ВСЕГДА говорило «low». Зелёный вердикт,
|
||||||
|
ни разу не подкреплённый проверкой геологии.
|
||||||
|
|
||||||
|
Поле убрано, а не переименовано: соседний блок `geology` честно отдаёт
|
||||||
|
`data_available: false`, когда данных нет, и потребителей у метки не было — ни
|
||||||
|
фронт, ни экспортёры, ни §19-allowlist чата её не читали.
|
||||||
|
|
||||||
|
Тесты смотрят на исходник эндпоинта: собрать полный ответ `/analyze` в юните
|
||||||
|
нельзя (нужны БД, OSM, НСПД), а утверждение проверяемо и так — метка не должна
|
||||||
|
существовать, и шум не должен участвовать в риск-блоке.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _risks_block_source() -> str:
|
||||||
|
"""Кусок analyze-эндпоинта, собирающий risks_block."""
|
||||||
|
from app.api.v1 import parcels
|
||||||
|
|
||||||
|
src = inspect.getsource(parcels)
|
||||||
|
начало = src.index("risks_block: dict[str, Any]")
|
||||||
|
конец = src.index("# 10) Market trend", начало)
|
||||||
|
return src[начало:конец]
|
||||||
|
|
||||||
|
|
||||||
|
def test_geology_risk_label_is_gone() -> None:
|
||||||
|
"""Головной: метки нет в ответе.
|
||||||
|
|
||||||
|
На origin/main она есть и на тихом участке без реки всегда равна 'low'.
|
||||||
|
"""
|
||||||
|
блок = _risks_block_source()
|
||||||
|
assert '"geology_risk_label"' not in блок, f"метка всё ещё выдаётся:\n{блок[:400]}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_no_longer_feeds_the_risk_label() -> None:
|
||||||
|
"""Контроль подмены: шум не должен участвовать в риск-метке.
|
||||||
|
|
||||||
|
Ловит «починку» через переименование — если метку назвать иначе, но по-прежнему
|
||||||
|
считать из noise_db_max, обман останется тем же.
|
||||||
|
"""
|
||||||
|
блок = _risks_block_source()
|
||||||
|
assert "noise_db_max" not in блок, f"шум по-прежнему участвует в риск-блоке:\n{блок[:400]}"
|
||||||
|
assert not re.search(
|
||||||
|
r'"(high|medium|low)"', блок
|
||||||
|
), f"в риск-блоке остались словесные градации риска:\n{блок[:400]}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_score_itself_is_preserved() -> None:
|
||||||
|
"""Контроль от переусердствования: сам noise_score остаётся — он измерен честно."""
|
||||||
|
блок = _risks_block_source()
|
||||||
|
assert '"noise_score"' in блок, "убран и измеренный noise_score — это перебор"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flood_zone_still_reported() -> None:
|
||||||
|
"""Контроль: flood_zone остаётся в ответе (его судьба — отдельный пункт #2934)."""
|
||||||
|
блок = _risks_block_source()
|
||||||
|
assert '"flood_zone"' in блок
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_consumer_reads_the_removed_field() -> None:
|
||||||
|
"""Контроль обоснования: поле действительно никем не читалось.
|
||||||
|
|
||||||
|
Утверждение «потребителей нет» проверяется, а не декларируется — иначе
|
||||||
|
удаление поля из публичного ответа держалось бы на слове.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.api.v1 import parcels
|
||||||
|
|
||||||
|
корень = Path(inspect.getsourcefile(parcels)).resolve().parents[4]
|
||||||
|
попадания: list[str] = []
|
||||||
|
for каталог, маски in (
|
||||||
|
(корень / "backend" / "app", ("*.py",)),
|
||||||
|
(корень / "frontend" / "src", ("*.ts", "*.tsx")),
|
||||||
|
):
|
||||||
|
for маска in маски:
|
||||||
|
for f in каталог.rglob(маска):
|
||||||
|
if "__fixtures__" in str(f) or "api-types" in f.name:
|
||||||
|
continue
|
||||||
|
текст = f.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
for n, строка in enumerate(текст.splitlines(), 1):
|
||||||
|
if "geology_risk_label" not in строка:
|
||||||
|
continue
|
||||||
|
# Комментарий, объясняющий удаление, — не потребитель. Проверка
|
||||||
|
# обязана отличать упоминание от использования, иначе краснеет
|
||||||
|
# на собственном объяснении правки.
|
||||||
|
голая = строка.lstrip()
|
||||||
|
комментарий = (
|
||||||
|
голая.startswith("#") or голая.startswith("//") or голая.startswith("*")
|
||||||
|
)
|
||||||
|
if комментарий:
|
||||||
|
continue
|
||||||
|
попадания.append(f"{f.relative_to(корень)}:{n}")
|
||||||
|
assert попадания == [], f"поле где-то ещё ЧИТАЕТСЯ (не в комментарии): {попадания}"
|
||||||
Loading…
Add table
Reference in a new issue