feat: статусы реагирования на АРМ ДДС и ошибки диспетчера D1–D6 (lct-33)

ТЗ называет обучаемого оператором ДДС, а его работа в системе-112 состоит
из одного действия: получить карточку и правильно проставить статус
реагирования с комментарием. Памятка заказчика посвящена этому целиком.
Наш АРМ умел два статуса из девяти.

- domain/statuses.py: девять статусов реагирования с автоматом переходов,
  семь статусов карточки, обязательные комментарии к отказам. Формулировки
  из памятки дословно — диспетчер должен узнать слова своего боевого АРМ.
- Автомат на сервере: фронт рисует только пришедшее в available. Отвергнутая
  отметка не попадает в журнал и возвращается текстом для курсанта.
- scoring/dispatcher.py: D1, D2, D3, D4, D6 детерминированно. D3 (отказ
  от профильного происшествия) проверяется по списку оповещения из ЕКП —
  без классификатора его было бы не на чем посчитать. D5 оставлен судье.
- Отметки D идут в отчёт рядом с E: в живой цепочке 112 → ДДС участвуют обе роли.
- Экран ДДС: таблица служб со статусами, поле комментария у отказов,
  журнал отметок, статус карточки красным в трёх случаях из семи.

Сквозной тест повис на ожидании station.state и вскрыл продуктовый дефект:
снимок уходил только в ответ на действие диспетчера, то есть список оповещения
он видел лишь после того, как что-то нажмёт. Теперь station.state отправляется
сразу за card.received.

Упрощения записаны в карточке: статусы «Проверена» и «Не завершено» не
считаются — первый требует главного специалиста, второй 48 часов.

148 тестов зелёных (23 новых), make typecheck чистый.
This commit is contained in:
Ivan Gerasimov 2026-09-20 08:23:55 +03:00
commit d4f0d0a0f8
14 changed files with 953 additions and 9 deletions

View file

@ -12,18 +12,32 @@ from pydantic import BaseModel
class ErrorCode(StrEnum):
"""E — ошибки приёма вызова, D — ошибки диспетчера ДДС.
Две роли, две таксономии. D1–D6 взяты из памятки заказчика дословно:
она перечисляет типовые нарушения с реальными примерами, и преподаватель
узнаёт их формулировки (docs/spec/DATASET.md).
"""
E1 = "E1"
E2 = "E2"
E3 = "E3"
E4 = "E4"
E5 = "E5"
E6 = "E6"
D1 = "D1"
D2 = "D2"
D3 = "D3"
D4 = "D4"
D5 = "D5"
D6 = "D6"
class FindingSource(StrEnum):
"""Кто выставил отметку. `judge` — единственный недетерминированный источник."""
SLOTS = "slots"
DISPATCHER = "dispatcher"
GROUND_TRUTH = "ground_truth"
TIMERS = "timers"
KIO = "kio"
@ -76,6 +90,42 @@ ERRORS: dict[ErrorCode, ErrorSpec] = {
detail="Карточка ушла в ДДС непригодной, снятие с контроля до подтверждения",
source=FindingSource.CHAIN,
),
ErrorCode.D1: ErrorSpec(
code=ErrorCode.D1,
title="Статус реагирования не проставлен",
detail="Карточка осталась без первичного статуса и ушла в «Не оповещено»",
source=FindingSource.DISPATCHER,
),
ErrorCode.D2: ErrorSpec(
code=ErrorCode.D2,
title="Статус не соответствует факту",
detail="«Принята» там, где работы проводиться не будут, и наоборот",
source=FindingSource.DISPATCHER,
),
ErrorCode.D3: ErrorSpec(
code=ErrorCode.D3,
title="Отказ от профильного происшествия",
detail="Служба есть в списке оповещения, значит происшествие её — отказ неправомерен",
source=FindingSource.DISPATCHER,
),
ErrorCode.D4: ErrorSpec(
code=ErrorCode.D4,
title="Нет комментария к отказу",
detail="«Не принята» и «Отказ от выполнения работ» без причины",
source=FindingSource.DISPATCHER,
),
ErrorCode.D5: ErrorSpec(
code=ErrorCode.D5,
title="Неполный комментарий",
detail="Не указано, куда передана информация и что сделал диспетчер",
source=FindingSource.JUDGE,
),
ErrorCode.D6: ErrorSpec(
code=ErrorCode.D6,
title="Нет статусов хода работ",
detail="Реагирование шло, но начало, прибытие и завершение не отмечены",
source=FindingSource.DISPATCHER,
),
}