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
|
|
@ -9,6 +9,7 @@
|
|||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from app.domain import ekp
|
||||
from app.domain.events import CallEndReason, Metric
|
||||
from app.domain.kio import KIO, missing_fields
|
||||
from app.domain.taxonomy import ERRORS, Competency, Finding, FindingSource
|
||||
|
|
@ -182,7 +183,27 @@ def evaluate(
|
|||
)
|
||||
|
||||
# ── классификация и маршрутизация, E2 ──
|
||||
if truth.incident_type is not None:
|
||||
#
|
||||
# Признаки — то, что оператор действительно делает в боевом АРМ-112: службу
|
||||
# он не выбирает, её считает система по ЕКП (docs/spec/DATASET.md). Поэтому
|
||||
# размеченный признаками сценарий оценивается по ним, а не по выбору ДДС.
|
||||
if truth.incident_code is not None:
|
||||
found = ekp.incident(truth.incident_code)
|
||||
expected = " · ".join(found.signs) if found else truth.incident_code
|
||||
actual = " · ".join(kio.signs) if kio.signs else "не проставлены"
|
||||
build.add(
|
||||
"incident_signs", "Признаки происшествия",
|
||||
actual, expected,
|
||||
passed=kio.incident_code == truth.incident_code,
|
||||
ref=f"классификатор ЕКП {ekp.reference().version}",
|
||||
finding=(
|
||||
f"Признаки «{actual}» дают {kio.incident_code or 'пустой код'}, "
|
||||
f"верные — «{expected}» ({truth.incident_code})"
|
||||
),
|
||||
)
|
||||
# Список оповещения производен: при верных признаках он верен всегда,
|
||||
# и штрафовать за него отдельно — штрафовать дважды за одно.
|
||||
elif truth.incident_type is not None:
|
||||
actual = kio.incident_type.value if kio.incident_type else "не указан"
|
||||
build.add(
|
||||
"incident_type", "Тип происшествия",
|
||||
|
|
@ -191,7 +212,7 @@ def evaluate(
|
|||
ref="классификатор происшествий",
|
||||
finding=f"Тип происшествия {actual}, верный — {truth.incident_type.value}",
|
||||
)
|
||||
if truth.dds is not None:
|
||||
if truth.dds is not None and truth.incident_code is None:
|
||||
actual = kio.dds.value if kio.dds else "не выбрана"
|
||||
build.add(
|
||||
"dds_choice", "Выбор ДДС",
|
||||
|
|
|
|||
Loading…
Reference in a new issue