feat: классификатор ЕКП — признаки вместо выбора службы (lct-32)
Оператор системы-112 службу не выбирает: он проставляет формализованные признаки происшествия, комбинация признаков даёт код ЕКП, а коду соответствует список оповещения, который система собирает сама (docs/spec/DATASET.md). Прежняя модель с полем dds из пяти значений оценивала действие, которого в боевой работе нет. - scripts/import_ekp.py (make ekp): книга заказчика → app/domain/ekp.json, 1283 кода, 61 служба, 23 группы. Разовый импорт, результат под гитом: читать xlsx в рантайме — лишняя зависимость и полсекунды на старте. - domain/ekp.py: справочник с ленивой загрузкой, каскад значений признаков, список оповещения с модификаторами (нет доступа, угроза людям, пострадавшие, газификация и ещё десяток). - КИО: поля signs, incident_code, notify. Последние два только на чтение и пересчитываются при каждой правке признаков; добавленная вручную служба не теряется, удалить службу нельзя — как в боевом АРМ. - GET /api/ekp/signs отдаёт один уровень признаков, а не дерево на полмегабайта. - Карточка на фронте: три каскадных селектора вместо выбора службы. - Оценка: метрика incident_signs (E2, маршрутизация). Для размеченных сценариев dds_choice больше не считается — список оповещения производен от признаков, и штрафовать за него отдельно значит наказать дважды за одну ошибку. Разбор книги оказался основной работой: имя службы лежит то в первой строке заголовка, то во второй, то склеено с модификатором; «Классификатор МЧС» — заголовок группы колонок, а не служба. Правила разбора в докстринге импорта. 113 тестов зелёных (14 новых в tests/test_ekp.py), make typecheck чистый.
This commit is contained in:
parent
ed0b13f060
commit
7212251112
16 changed files with 522 additions and 19 deletions
|
|
@ -102,7 +102,10 @@ class KIO(BaseModel):
|
|||
intercom_code: str | None = None # спрашивается чек-листом 03, без него скорая стоит у двери
|
||||
coords: Coords | None = None
|
||||
|
||||
# Происшествие
|
||||
# Происшествие. Оператор проставляет признаки, остальное считает система
|
||||
# по ЕКП (domain/ekp.py) — службу он не выбирает.
|
||||
signs: list[str] = Field(default_factory=list, max_length=3)
|
||||
incident_code: str | None = Field(default=None, description="Код ЕКП по признакам")
|
||||
incident_type: IncidentType | None = None
|
||||
description: str | None = None
|
||||
victims_count: int | None = None
|
||||
|
|
@ -110,8 +113,12 @@ class KIO(BaseModel):
|
|||
life_threat: bool = False
|
||||
evacuation_needed: bool = False
|
||||
|
||||
# ДДС
|
||||
dds: DDSCode | None = None
|
||||
# ДДС. `notify` считается из признаков и правится только системой:
|
||||
# в боевом АРМ оператор может добавить службу вручную, но не удалить.
|
||||
notify: list[str] = Field(default_factory=list, description="Список оповещения")
|
||||
dds: DDSCode | None = Field(
|
||||
default=None, description="Устаревшее: одна служба. Заменено списком оповещения"
|
||||
)
|
||||
dispatch_order_at: datetime | None = None
|
||||
arrival_at: datetime | None = None
|
||||
|
||||
|
|
@ -124,7 +131,7 @@ class KIO(BaseModel):
|
|||
|
||||
#: Поля, которые курсант не редактирует: их проставляет система.
|
||||
READ_ONLY_FIELDS: frozenset[str] = frozenset(
|
||||
{"card_id", "registered_at", "caller_number", "response_status"}
|
||||
{"card_id", "registered_at", "caller_number", "response_status", "incident_code", "notify"}
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -152,6 +159,23 @@ def missing_fields(card: KIO, required: list[str]) -> list[str]:
|
|||
return empty
|
||||
|
||||
|
||||
def derive_incident(card: KIO) -> KIO:
|
||||
"""Пересчитать код происшествия и список оповещения по признакам.
|
||||
|
||||
Вызывается после каждой правки признаков: в боевом АРМ список оповещения
|
||||
пересобирается сам, и это главное, чему учит карточка.
|
||||
"""
|
||||
from app.domain import ekp
|
||||
|
||||
found = ekp.by_signs(card.signs)
|
||||
data = card.model_dump()
|
||||
data["incident_code"] = found.code if found else None
|
||||
# Добавленную вручную службу не теряем: удалить её оператор не может.
|
||||
manual = [name for name in card.notify if name not in ekp.notify_list(data["incident_code"] or "")]
|
||||
data["notify"] = (ekp.notify_list(found.code) if found else []) + manual
|
||||
return KIO.model_validate(data)
|
||||
|
||||
|
||||
def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO:
|
||||
"""Применить дельту `kio.patch`. Служебные поля игнорируются.
|
||||
|
||||
|
|
@ -168,4 +192,5 @@ def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO:
|
|||
nested = data.get(head) or {}
|
||||
nested[tail] = value
|
||||
data[head] = nested
|
||||
return KIO.model_validate(data)
|
||||
updated = KIO.model_validate(data)
|
||||
return derive_incident(updated) if "signs" in fields else updated
|
||||
|
|
|
|||
Loading…
Reference in a new issue