Оператор системы-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 чистый.
48 lines
2.4 KiB
Python
48 lines
2.4 KiB
Python
"""Какая метрика каким кодом ошибки и какой компетенцией размечается.
|
||
|
||
Это методика, а не вычисление: таблица читается глазами и сверяется
|
||
с docs/product/METHODOLOGY.md. Считает метрики gost.py, сюда он только смотрит.
|
||
"""
|
||
|
||
from app.domain.taxonomy import Competency, ErrorCode
|
||
|
||
#: Метрика → (код ошибки при провале, компетенция радара).
|
||
METRIC_MAP: dict[str, tuple[ErrorCode, Competency]] = {
|
||
"answer_time": (ErrorCode.E3, Competency.INTAKE),
|
||
"dds_chain": (ErrorCode.E6, Competency.CARD),
|
||
"callback": (ErrorCode.E3, Competency.INTAKE),
|
||
"checklist_completeness": (ErrorCode.E1, Competency.INTERVIEW),
|
||
"interview_time": (ErrorCode.E3, Competency.NORMS),
|
||
"incident_signs": (ErrorCode.E2, Competency.ROUTING),
|
||
"incident_type": (ErrorCode.E2, Competency.ROUTING),
|
||
"dds_choice": (ErrorCode.E2, Competency.ROUTING),
|
||
"address": (ErrorCode.E5, Competency.CARD),
|
||
"victims_count": (ErrorCode.E5, Competency.CARD),
|
||
"required_fields": (ErrorCode.E5, Competency.CARD),
|
||
}
|
||
|
||
#: Вес метрики в детерминированной оценке.
|
||
#:
|
||
#: **Предварительные значения, требуют утверждения методистом.** Без весов все
|
||
#: метрики равны, и курсант, не задавший ни одного вопроса, но заполнивший
|
||
#: карточку руками, получает 87 из 100: «ответ за секунду» стоит столько же,
|
||
#: сколько «добыл все обязательные факты». Опрос — то, ради чего существует
|
||
#: тренажёр, поэтому он весит больше всего.
|
||
METRIC_WEIGHTS: dict[str, float] = {
|
||
"checklist_completeness": 4.0,
|
||
"incident_signs": 2.0,
|
||
"incident_type": 2.0,
|
||
"dds_choice": 2.0,
|
||
"address": 2.0,
|
||
"required_fields": 2.0,
|
||
"interview_time": 1.5,
|
||
"victims_count": 1.0,
|
||
"answer_time": 1.0,
|
||
"callback": 1.0,
|
||
"dds_chain": 2.0,
|
||
}
|
||
|
||
#: Вес детерминированного слоя в итоговой оценке. Остальное — LLM-судья
|
||
#: на мягкие критерии (E4), и не больше (docs/arch/BACKEND.md).
|
||
DETERMINISTIC_WEIGHT = 0.6
|
||
JUDGE_WEIGHT = 0.4
|