- rules_analysis._build_zone_reason: детерминированный текст по тем же
факторам, что считает WeightedScorer — рельеф (лесистость %), вода
(расстояние + акцент берегов при не_умеет_плавать), дороги (плотность +
вероятное следование велосипеда), НП (ориентир), совпадение с направлением
последнего свидетеля (точное/соседний румб), полосы психотипа. Финал —
балл оценки.
- ФИКС: weight 'direction' (0.07) был мёртвым — direction_match никогда не
передавался в score_zone (всегда дефолт 0.5); теперь 1.0 при совпадении
с last_seen_direction.
- Фронт уже отображает reasoning на карточках зон — бэкенд-фикс достаточен.
- Тесты: 7 новых на пояснения (детерминированность, факторы, фолбэк). 238 passed.
1. Рекомендации не предписывают перекрывать водоёмы/ж/д — оператор ПРОВЕРЯЕТ
их (правки в rules_analysis, scoring_service ×2 копии, water.py + доки).
2. Геокодирование: район нужен (дубли улиц между районами) — подсказка формата
«пункт, район, адрес» и текст ошибки.
198 passed.
Решение Виктора: СП(лес) 0.5→1.0. Прежняя таблица учитывала лес дважды
(в НормС и в СП) — подросток 14 лет за 3 часа получал 2.55 км вместо
реалистичных 5.1. Согласуется с априором ПСО (94% находок в 3 км):
радиусы стали верхне-перцентилями, не медианой.
- terrain_map пересчитан относительно среднего леса: лес/простой лес/поле/
луг/город = 1.0; сложный/густой лес 0.5; болото 0.4; горы/овраг 0.6;
дорога/тропа 0.8; дефолт 1.0.
- Контрольные точки тестов пересчитаны (6 шт: 1.92→3.84, 0.33→0.665,
2.43→2.7, 5.4→10.8, 13.5→15.0, stats 0.5→1.0) + операционный тест
подростка 5.1 км.
- settlement_score пока НЕ тронут — на обдумывание (Виктор).
193 passed.
Ближайшая вода бралась по bbox сектора: объект в углу bbox, но вне
45°-пирога, попадал в выборку — вода почти не различала направления
(1.393 км у N/NE/E с одной воды в центре). Теперь строго внутри сектора:
water 1.393–2.337 км по направлениям — честная география.
Два бага в одной строке: водные дистанции делились на cos (вместо
умножения, как дороги) и отдавались как км без /1000. Результат —
850.671 «км» до озера в Браславе (реально 271 м).
Верифицировано против geography: merc×cos = 271.0 м ≈ geo 271.5 м
(дельта 0.1%). Теперь Браслав SE: water 0.271 км, settle 0.119 км.
197 passed.
Закономерность «приоритетная зона вверх-вправо»: score_zone умножает
weight × forest_pct, ожидая ДОЛЮ (тесты скоринга 0.3–0.8), а гео-сервисы
(osm_local и legacy Overpass calculate_forest_coverage) отдавали
ПРОЦЕНТЫ 0..100. Лес получил фактический вес в 100 раз больше задумки —
топ зон систематически занимали чуть более зелёные сектора.
Фикс: osm_local.calculate_forest_coverage возвращают долю 0..1.
Разброс скорингов по Минску: 1.244 → 0.032 (честно однородный город).
Остаточные различия — вода/дороги; дифференциация плотностей город/село —
ревью скоринга (B1), коэффициенты не тронуты.
+регресс-тест 0<=forest_pct<=1. 197 passed.
Определение специфических рекомендаций (матрица профилей §8):
1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
- /api/v1/water/{case_id} отдаёт railway=rail как LineString
(без service/industrial/military веток), кэш общий v2;
- railway_warning «перекрыть/проверить немедленно» по профилям;
- SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.
2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
- раньше чекбокс влиял только на текст, в скоринге был пробел;
- derive в analyze._derive_profiles — работает и для closed_cases.
3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
«вне поведенческой модели» с пояснением (vector_tasks B12:
профили без аналога не выдавать за учтённые); блок на фронте
в карточке здоровья.
Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
- GET /api/v1/water/{case_id}: полигоны водоёмов/болот OSM вокруг ТНП
(Overpass, natural=water+wetland, кэш 72ч, circuit breaker от geo_service);
бейдж-предупреждение по профилям случая (РАС/эпилепсия/не умеет плавать)
- geo_service: цепочка зеркал Overpass (maps.mail.ru → overpass-api.de):
основной сервер имеет AAAA, на хостах без IPv6-маршрута httpx падал
ConnectError'ом до IPv4-фолбэка; на CT108 фапало стабильно
- SearchMap: слой водоёмов под зонами (синие полигоны, болота пунктиром),
попапы с площадью/дистанцией от ТНП; AnalysisResult грузит слой,
показывает предупреждение профиля и счётчик водоёмов
- проверено на CT108: РАС-кейс (Минск) — 43 объекта, витя-кейс
(Каменец-борисовская обл.) — 7 объектов, включая водохранилище Загацце
Сверка с методикой ПСР ред. 12.17 (Евдокимов, Лейтес):
- ночь: 0.5 → 0.0 — методичка §12.3.5 прямо даёт «ночь = 0 км»:
без фонаря в лесу ночью пострадавший не перемещается.
Ранее 0.5 ЗАВЫШАЛО радиус ночного поиска.
- сумерки/вечер: 0.7 → 0.5 — «за час до заката скорость 50% от обычной».
B5 переносил 0.7 из старого фронта вопреки методичке.
- get_base_speed: документирована семантика «скорость смещения»
(не асфальтовая НормС из §12.3.2) — оператору не нужно вводить
асфальтовую скорость, она уже свёрнута; СП = поправка конкретной
местности поверх базового среднего леса.
Тесты обновлены под методичку, добавлен тест ночи = 0.
201 passed, 0 failed.
_extract_json_payload handles a json/JSON/bare fence, raw JSON and JSON
embedded in prose; any unparseable response or contract violation now
degrades to the deterministic scoring_service instead of raising.
Also guards the response envelope itself (content[0].text).
B3: single home for recommendation scoring
services/recommendation_service.py holds the rules; routers/stats.py and
backend/services/stats_service.py both delegate to it. Unified rules are the
union of the two old copies: same weights/threshold, substring matching
(superset of the old exact match), tolerant key aliases, health_flags rule
kept. Endpoint response contract unchanged.
Plus: Overpass circuit breaker and concurrent zone queries in geo_service -
128 sequential calls per analysis no longer each burn a connect timeout when
the host has no outbound network.
Tests: 152 -> 194 passed.
backend/routers/analyze.py imports services.* (root package), not
backend.services.* - so the earlier A1/A2/A3 edits landed in an unused
duplicate. Also COPY/mount services/ so the container can start at all
(ModuleNotFoundError: No module named services).