feat: ж/д слой + cant_swim в скоринг + профили вне модели

Определение специфических рекомендаций (матрица профилей §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).
This commit is contained in:
2026-09-09 13:12:55 +03:00
parent 98e8e58023
commit f20080305d
2069 changed files with 803865 additions and 97 deletions
+23
View File
@@ -87,6 +87,26 @@ def _as_case_data(payload: AnalysisRequest) -> dict[str, Any]:
}
# Диагнозы, которые форма собирает, но поведенческая модель не покрывает
# (vector_tasks.md B12: профили без аналога/данных — честно помечать «вне модели»).
UNMODELED_DIAGNOSES: dict[str, str] = {
'ДЦП': 'Двигательные нарушения — модель не учитывает; учесть вручную при планировании (доступность местности, темп).',
'слабое_зрение': 'Слабое зрение — модель не учитывает; вероятность следования по открытым/звуковым ориентирам выше.',
'слабый_слух': 'Слабый слух — модель не учитывает; голосовой поиск менее эффективен, приоритет визуальным сигналам.',
}
def _unmodeled_profiles(case_data: dict[str, Any]) -> list[dict[str, str]]:
"""Диагнозы вне поведенческой модели (не влияют на скоринг) + пояснение."""
out: list[dict[str, str]] = []
for diag in (case_data.get('diagnosis_type') or []):
d = str(diag).lower()
for key, note in UNMODELED_DIAGNOSES.items():
if d == key.lower() and not any(o['profile'] == key for o in out):
out.append({'profile': key, 'note': note})
return out
def _derive_profiles(case_data: dict[str, Any]) -> list[str]:
"""
Формирует список поведенческих профилей из диагнозов и транспорта, если они
@@ -112,6 +132,8 @@ def _derive_profiles(case_data: dict[str, Any]) -> list[str]:
derived.append('велосипед')
elif transport == 'scooter':
derived.append('самокат')
if case_data.get('cant_swim'):
derived.append('не_умеет_плавать')
seen: set[str] = set()
out: list[str] = []
for p in derived:
@@ -213,6 +235,7 @@ async def analyze_case(payload: AnalysisRequest) -> dict[str, Any]:
'distance_multiplier': scorer.distance_multiplier,
'active_profiles': scorer.get_active_profiles_info(),
'critical_warnings': scorer.critical_warnings,
'unmodeled_profiles': _unmodeled_profiles(case_data),
'urgency': claude_result.urgency,
'primary_zones': [zone.model_dump() for zone in claude_result.primary_zones],
'search_radius_km': claude_result.search_radius_km,