lct-hack/backend/app/api/http/ekp.py
Ivan Gerasimov 7212251112 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 чистый.
2026-09-19 20:11:50 +03:00

50 lines
2.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Справочник ЕКП по 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
),
}