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
50
backend/app/api/http/ekp.py
Normal file
50
backend/app/api/http/ekp.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Справочник ЕКП по HTTP: группы происшествий и значения признаков.
|
||||
|
||||
Отдавать его курсанту безопасно, в отличие от сценария: это классификатор,
|
||||
а не содержимое вызова. Оператор в боевом АРМ видит тот же список и выбирает
|
||||
из него — скрывать нечего.
|
||||
|
||||
Значения признака зависят от уже выбранных: «жилой дом» → «балкон» → «открытое
|
||||
пламя». Поэтому уровень запрашивается отдельно, а не отдаётся деревом целиком:
|
||||
дерево на 1283 листа весит полмегабайта, а нужно из него три десятка строк.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.domain import ekp
|
||||
|
||||
router = APIRouter(prefix="/api/ekp", tags=["ekp"])
|
||||
|
||||
|
||||
@router.get("/groups")
|
||||
async def groups() -> dict:
|
||||
"""Группы происшествий — «категории событий» из ТЗ, тема занятия."""
|
||||
return {
|
||||
"version": ekp.reference().version,
|
||||
"groups": [group.model_dump() for group in ekp.groups()],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/signs")
|
||||
async def signs(prefix: list[str] = Query(default=[])) -> dict:
|
||||
"""Значения следующего признака при уже выбранных.
|
||||
|
||||
Пустой `prefix` — значения первого уровня. Возвращается и найденное
|
||||
происшествие, если признаков хватило: по нему курсант видит, что именно
|
||||
он классифицировал, до того, как отправит карточку.
|
||||
"""
|
||||
level = len(prefix) + 1
|
||||
found = ekp.by_signs(prefix) if prefix else None
|
||||
return {
|
||||
"level": level,
|
||||
"values": ekp.signs_at_level(level, prefix) if level <= 3 else [],
|
||||
"incident": (
|
||||
{
|
||||
"code": found.code,
|
||||
"type": found.type,
|
||||
"notify": ekp.notify_list(found.code),
|
||||
}
|
||||
if found
|
||||
else None
|
||||
),
|
||||
}
|
||||
Loading…
Reference in a new issue