каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
"""Карточка информационного обмена (КИО).
|
|
|
|
|
|
|
|
|
|
|
|
Это не дизайн формы, это норматив: docs/spec/NORMATIVES.md#карточка-информационного-обмена-кио
|
|
|
|
|
|
Обязательность полей задаётся сценарием (`required_fields`), а не моделью:
|
|
|
|
|
|
в ложном вызове число пострадавших не требуется, в ЧС требуется.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from enum import StrEnum
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
from uuid import UUID, uuid4
|
|
|
|
|
|
|
2026-09-27 16:52:11 +03:00
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
from app.domain.classifiers import DDSCode, IncidentType
|
|
|
|
|
|
|
|
|
|
|
|
|
lct-01: контракт закрыт — координаты, обязательные поля, коды ошибок
Координаты были представлены дважды: кортежем в КИО и объектом в данных
ЭРА-ГЛОНАСС. Теперь Coords {lat, lon} везде — в кортеже не видно, где
широта, и ошибка всплывает на карте у диспетчера, а не в типах.
required_fields едет в call.incoming и session.snapshot: обязательность
полей задаёт сценарий, без списка АРМ не подсветит незаполненное поле,
и курсант узнаёт о неполноте карточки только из разбора.
Канал error получил коды (ErrorKind, восемь значений) — фронт разбирает
код, а не текст сообщения. Имя не ErrorCode: оно занято таксономией E1–E6,
два одинаковых имени дали бы коллизию в generated.ts.
Добавлены поля, которые спрашивает чек-лист, а записать было некуда:
куда идёт дым, уехал ли нарушитель, код домофона.
Документы догнали код: mkh → gkh, payload transcript.append с якорем ref,
причина director у tts.cancel, attempt и stopped у таймеров.
2026-09-15 19:48:56 +03:00
|
|
|
|
class Coords(BaseModel):
|
|
|
|
|
|
"""Координаты одним представлением на весь контракт.
|
|
|
|
|
|
|
|
|
|
|
|
Именованные поля, а не кортеж: в `[55.75, 37.61]` невозможно увидеть,
|
|
|
|
|
|
где широта, а где долгота, и ошибка всплывёт на карте, а не в типах.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
lat: float
|
|
|
|
|
|
lon: float
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
class ResponseStatus(StrEnum):
|
|
|
|
|
|
REGISTERED = "registered"
|
|
|
|
|
|
TRANSFERRED = "transferred"
|
|
|
|
|
|
IN_PROGRESS = "in_progress"
|
|
|
|
|
|
CLOSED = "closed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FireDetails(BaseModel):
|
|
|
|
|
|
"""Уточняющие поля 01 — МЧС."""
|
|
|
|
|
|
|
|
|
|
|
|
fire_nature: str | None = None
|
|
|
|
|
|
object_kind: str | None = None
|
|
|
|
|
|
floors: int | None = None
|
|
|
|
|
|
gasified: bool | None = None
|
|
|
|
|
|
people_inside: bool | None = None
|
lct-01: контракт закрыт — координаты, обязательные поля, коды ошибок
Координаты были представлены дважды: кортежем в КИО и объектом в данных
ЭРА-ГЛОНАСС. Теперь Coords {lat, lon} везде — в кортеже не видно, где
широта, и ошибка всплывает на карте у диспетчера, а не в типах.
required_fields едет в call.incoming и session.snapshot: обязательность
полей задаёт сценарий, без списка АРМ не подсветит незаполненное поле,
и курсант узнаёт о неполноте карточки только из разбора.
Канал error получил коды (ErrorKind, восемь значений) — фронт разбирает
код, а не текст сообщения. Имя не ErrorCode: оно занято таксономией E1–E6,
два одинаковых имени дали бы коллизию в generated.ts.
Добавлены поля, которые спрашивает чек-лист, а записать было некуда:
куда идёт дым, уехал ли нарушитель, код домофона.
Документы догнали код: mkh → gkh, payload transcript.append с якорем ref,
причина director у tts.cancel, attempt и stopped у таймеров.
2026-09-15 19:48:56 +03:00
|
|
|
|
smoke_spread: str | None = None # куда идёт дым — пункт чек-листа 01
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PoliceDetails(BaseModel):
|
|
|
|
|
|
"""Уточняющие поля 02 — полиция."""
|
|
|
|
|
|
|
|
|
|
|
|
offence_kind: str | None = None
|
|
|
|
|
|
suspects: str | None = None
|
lct-01: контракт закрыт — координаты, обязательные поля, коды ошибок
Координаты были представлены дважды: кортежем в КИО и объектом в данных
ЭРА-ГЛОНАСС. Теперь Coords {lat, lon} везде — в кортеже не видно, где
широта, и ошибка всплывает на карте у диспетчера, а не в типах.
required_fields едет в call.incoming и session.snapshot: обязательность
полей задаёт сценарий, без списка АРМ не подсветит незаполненное поле,
и курсант узнаёт о неполноте карточки только из разбора.
Канал error получил коды (ErrorKind, восемь значений) — фронт разбирает
код, а не текст сообщения. Имя не ErrorCode: оно занято таксономией E1–E6,
два одинаковых имени дали бы коллизию в generated.ts.
Добавлены поля, которые спрашивает чек-лист, а записать было некуда:
куда идёт дым, уехал ли нарушитель, код домофона.
Документы догнали код: mkh → gkh, payload transcript.append с якорем ref,
причина director у tts.cancel, attempt и stopped у таймеров.
2026-09-15 19:48:56 +03:00
|
|
|
|
suspect_fled: bool | None = None # уехал ли нарушитель — пункт чек-листа 02
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
vehicle: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MedicalDetails(BaseModel):
|
|
|
|
|
|
"""Уточняющие поля 03 — медицина."""
|
|
|
|
|
|
|
|
|
|
|
|
reason: str | None = None
|
|
|
|
|
|
conscious: bool | None = None
|
|
|
|
|
|
breathing: bool | None = None
|
|
|
|
|
|
can_move: bool | None = None
|
|
|
|
|
|
age: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UtilityDetails(BaseModel):
|
|
|
|
|
|
"""Уточняющие поля 04 / ЖКХ."""
|
|
|
|
|
|
|
|
|
|
|
|
failure_kind: str | None = None
|
|
|
|
|
|
scale: str | None = None
|
|
|
|
|
|
threat_to_residents: bool | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KIO(BaseModel):
|
|
|
|
|
|
"""Полная карточка. Наблюдателям уходит целиком (`kio.state`),
|
lct-12: детерминированная оценка по ГОСТ, таксономия, радар
Каждая метрика — факт против норматива со ссылкой: «94 с при ≤ 75 с,
ГОСТ Р 22.7.03-2021», а не балл. По недобытому факту — отдельная отметка
E1 с эталонным вопросом: в разборе нужен конкретный вопрос, не процент.
Радар — проекция тех же метрик без пересчёта весов, коммуникация без
судьи не рисуется нулём.
Таймер, не остановленный событием, — провал, а не зачёт: время
недоказуемо, операция не завершена. Метрика, которую нечем посчитать
(полнота опроса без модели эмбеддингов), видна как «не посчитано» —
молча выброшенная выглядела бы пройденной.
Найдено противоречие: таймеры опроса (75 с) и оповещения ДДС (60 с)
стартовали на ответе и останавливались передачей в ДДС — один отрезок,
два лимита. Опрос за законные 70 с давал E3, а на экране курсанта
краснел таймер посреди нормального разговора. События «опрос закончен»
в контракте нет, поэтому dds_notify снят с учёта, вопрос записан в
CONTRACT.md.
KIO проверяет присваивание: card.dds = "03" клало в карточку сырую
строку вместо кода ДДС, и падала уже оценка, далеко от места ошибки.
2026-09-17 14:06:16 +03:00
|
|
|
|
курсанту — дельтой (`kio.patch`).
|
|
|
|
|
|
|
|
|
|
|
|
Присваивание проверяется: без этого `card.dds = "03"` кладёт в карточку
|
|
|
|
|
|
сырую строку вместо кода ДДС, и падает уже оценка, далеко от места ошибки.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(validate_assignment=True)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
# Служебное — заполняется системой
|
|
|
|
|
|
card_id: UUID = Field(default_factory=uuid4)
|
|
|
|
|
|
registered_at: datetime | None = None
|
|
|
|
|
|
response_status: ResponseStatus = ResponseStatus.REGISTERED
|
|
|
|
|
|
|
|
|
|
|
|
# Звонящий
|
|
|
|
|
|
caller_number: str | None = None
|
|
|
|
|
|
caller_name: str | None = None
|
|
|
|
|
|
caller_contact: str | None = None
|
2026-09-24 01:10:49 +03:00
|
|
|
|
phone_on_scene: str | None = None
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
language: str = "ru"
|
|
|
|
|
|
|
|
|
|
|
|
# Место
|
|
|
|
|
|
okato: str | None = None
|
|
|
|
|
|
address: str | None = None
|
|
|
|
|
|
street: str | None = None
|
|
|
|
|
|
building: str | None = None
|
|
|
|
|
|
entrance: str | None = None
|
|
|
|
|
|
floor: str | None = None
|
lct-01: контракт закрыт — координаты, обязательные поля, коды ошибок
Координаты были представлены дважды: кортежем в КИО и объектом в данных
ЭРА-ГЛОНАСС. Теперь Coords {lat, lon} везде — в кортеже не видно, где
широта, и ошибка всплывает на карте у диспетчера, а не в типах.
required_fields едет в call.incoming и session.snapshot: обязательность
полей задаёт сценарий, без списка АРМ не подсветит незаполненное поле,
и курсант узнаёт о неполноте карточки только из разбора.
Канал error получил коды (ErrorKind, восемь значений) — фронт разбирает
код, а не текст сообщения. Имя не ErrorCode: оно занято таксономией E1–E6,
два одинаковых имени дали бы коллизию в generated.ts.
Добавлены поля, которые спрашивает чек-лист, а записать было некуда:
куда идёт дым, уехал ли нарушитель, код домофона.
Документы догнали код: mkh → gkh, payload transcript.append с якорем ref,
причина director у tts.cancel, attempt и stopped у таймеров.
2026-09-15 19:48:56 +03:00
|
|
|
|
intercom_code: str | None = None # спрашивается чек-листом 03, без него скорая стоит у двери
|
|
|
|
|
|
coords: Coords | None = None
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
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
|
|
|
|
# Происшествие. Оператор проставляет признаки, остальное считает система
|
|
|
|
|
|
# по ЕКП (domain/ekp.py) — службу он не выбирает.
|
2026-09-21 20:10:57 +03:00
|
|
|
|
incident_group: int | None = None
|
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
|
|
|
|
signs: list[str] = Field(default_factory=list, max_length=3)
|
|
|
|
|
|
incident_code: str | None = Field(default=None, description="Код ЕКП по признакам")
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
incident_type: IncidentType | None = None
|
|
|
|
|
|
description: str | None = None
|
|
|
|
|
|
victims_count: int | None = None
|
|
|
|
|
|
is_emergency: bool = False
|
|
|
|
|
|
life_threat: bool = False
|
|
|
|
|
|
evacuation_needed: bool = False
|
|
|
|
|
|
|
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
|
|
|
|
# ДДС. `notify` считается из признаков и правится только системой:
|
|
|
|
|
|
# в боевом АРМ оператор может добавить службу вручную, но не удалить.
|
|
|
|
|
|
notify: list[str] = Field(default_factory=list, description="Список оповещения")
|
2026-09-27 16:52:11 +03:00
|
|
|
|
#: Службы, добавленные оператором вручную из каталога. Автоматический список
|
|
|
|
|
|
#: не заменяют и в оценку по эталону не входят.
|
|
|
|
|
|
notify_extra: list[str] = Field(
|
|
|
|
|
|
default_factory=list, max_length=20, description="Дополнительно оповестить"
|
|
|
|
|
|
)
|
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
|
|
|
|
dds: DDSCode | None = Field(
|
|
|
|
|
|
default=None, description="Устаревшее: одна служба. Заменено списком оповещения"
|
|
|
|
|
|
)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
dispatch_order_at: datetime | None = None
|
|
|
|
|
|
arrival_at: datetime | None = None
|
|
|
|
|
|
|
|
|
|
|
|
# Уточняющие по службе
|
|
|
|
|
|
fire: FireDetails | None = None
|
|
|
|
|
|
police: PoliceDetails | None = None
|
|
|
|
|
|
medical: MedicalDetails | None = None
|
|
|
|
|
|
utility: UtilityDetails | None = None
|
|
|
|
|
|
|
2026-09-27 16:52:11 +03:00
|
|
|
|
@field_validator("notify_extra")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def known_catalog_services(cls, names: list[str]) -> list[str]:
|
|
|
|
|
|
"""Только службы каталога, без повторов: свободный текст в адресатах
|
|
|
|
|
|
карточки на АРМ ДДС выглядел бы как настоящая служба."""
|
|
|
|
|
|
from app.domain import ekp
|
|
|
|
|
|
|
|
|
|
|
|
known = ekp.catalog_names()
|
|
|
|
|
|
unknown = [name for name in names if name not in known]
|
|
|
|
|
|
if unknown:
|
|
|
|
|
|
raise ValueError(f"нет в каталоге служб: {', '.join(unknown[:3])}")
|
|
|
|
|
|
return list(dict.fromkeys(names))
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
#: Поля, которые курсант не редактирует: их проставляет система.
|
|
|
|
|
|
READ_ONLY_FIELDS: frozenset[str] = frozenset(
|
2026-09-26 18:12:27 +03:00
|
|
|
|
{
|
|
|
|
|
|
"card_id", "registered_at", "caller_number", "response_status",
|
|
|
|
|
|
"incident_code", "notify", "dispatch_order_at", "arrival_at",
|
|
|
|
|
|
}
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
# Поля, которые реально можно заполнить в форме курсантского КИО. Держим
|
|
|
|
|
|
# отдельный allowlist: наличие атрибута в модели ещё не означает, что форма
|
|
|
|
|
|
# умеет его показать и отправить (например, coords или служебные timestamps).
|
|
|
|
|
|
EDITABLE_KIO_FIELDS: frozenset[str] = frozenset({
|
|
|
|
|
|
"caller_name", "caller_contact", "phone_on_scene", "language",
|
|
|
|
|
|
"okato", "address", "street", "building", "entrance", "floor",
|
|
|
|
|
|
"intercom_code", "description", "incident_group", "signs",
|
|
|
|
|
|
"incident_type", "victims_count", "is_emergency", "life_threat",
|
2026-09-27 16:52:11 +03:00
|
|
|
|
"evacuation_needed", "dds", "notify_extra",
|
2026-09-26 18:12:27 +03:00
|
|
|
|
"fire.fire_nature", "fire.object_kind", "fire.floors", "fire.gasified",
|
|
|
|
|
|
"fire.people_inside", "fire.smoke_spread",
|
|
|
|
|
|
"police.offence_kind", "police.suspects", "police.suspect_fled",
|
|
|
|
|
|
"police.vehicle",
|
|
|
|
|
|
"medical.reason", "medical.conscious", "medical.breathing",
|
|
|
|
|
|
"medical.can_move", "medical.age",
|
|
|
|
|
|
"utility.failure_kind", "utility.scale", "utility.threat_to_residents",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
|
|
|
|
|
|
def get_field(card: KIO, path: str) -> Any:
|
|
|
|
|
|
"""Значение поля по пути вида `floor` или `fire.floors`."""
|
|
|
|
|
|
value: Any = card
|
|
|
|
|
|
for part in path.split("."):
|
|
|
|
|
|
if value is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
value = getattr(value, part, None)
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def missing_fields(card: KIO, required: list[str]) -> list[str]:
|
|
|
|
|
|
"""Незаполненные обязательные поля — основание отметки E5.
|
|
|
|
|
|
|
|
|
|
|
|
Обязательность приходит из сценария, пустой список означает,
|
|
|
|
|
|
что сценарий требований к карточке не предъявляет.
|
|
|
|
|
|
"""
|
|
|
|
|
|
empty: list[str] = []
|
|
|
|
|
|
for path in required:
|
|
|
|
|
|
value = get_field(card, path)
|
|
|
|
|
|
if value is None or (isinstance(value, str) and not value.strip()):
|
|
|
|
|
|
empty.append(path)
|
|
|
|
|
|
return empty
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: исход вызова — не каждый звонок заканчивается карточкой (lct-36)
Весь продукт стоял на допущении «звонок → карточка → выезд». В билетах
заказчика оно нарушается намеренно: «поругался с продавцом Мегафон» — справка,
а вызов из Волгоградской области передаётся в систему-112 другого субъекта.
Курсант, заведший карточку на такой вызов, занял расчёт зря, и прежняя оценка
этого не видела — наоборот, награждала за полноту заполнения.
- Outcome в домене, поле outcome в сценарии, событие call.resolve у курсанта
и две кнопки на АРМ. Отдельное действие, а не «положил трубку»: система
должна отличить осознанное решение от брошенного вызова.
- Метрика outcome (E2, вес 3 — лишний выезд дороже неточного признака).
- Там, где карточка не заводится, метрики карточки не считаются вовсе.
Полнота опроса считается: передать вызов не значит не опрашивать.
- call.resolve останавливает норматив опроса, как и передача карточки.
Попутно (lct-34): библиотека стала вложенной — scenarios/tickets/, поля
ticket и position, чек-листы для медицины, полиции и ЖКХ. Перенесён билет 1
целиком: три вызова, три классификатора, третий — из другого региона.
Найдено по ходу: модификаторы списка оповещения не были подключены ни к чему.
В классификаторе скорая добавляется к массовой драке признаком «пострадавшие»,
но взять его было неоткуда. Теперь модификаторы берутся из карточки —
victims_count, life_threat, evacuation_needed, fire.gasified, — и список
оповещения пересобирается при правке любого из этих полей.
161 тест зелёный (13 новых), make typecheck чистый.
2026-09-20 08:33:30 +03:00
|
|
|
|
#: Поля карточки, которые меняют состав списка оповещения. В боевом АРМ это
|
|
|
|
|
|
#: те же «признаки», только не про тип происшествия, а про обстоятельства:
|
|
|
|
|
|
#: пострадавшие поднимают скорую, газификация — МОСГАЗ (domain/ekp.py).
|
|
|
|
|
|
MODIFIER_FIELDS: frozenset[str] = frozenset(
|
2026-09-21 20:10:57 +03:00
|
|
|
|
{"incident_group", "signs", "victims_count", "life_threat", "evacuation_needed", "fire.gasified"}
|
feat: исход вызова — не каждый звонок заканчивается карточкой (lct-36)
Весь продукт стоял на допущении «звонок → карточка → выезд». В билетах
заказчика оно нарушается намеренно: «поругался с продавцом Мегафон» — справка,
а вызов из Волгоградской области передаётся в систему-112 другого субъекта.
Курсант, заведший карточку на такой вызов, занял расчёт зря, и прежняя оценка
этого не видела — наоборот, награждала за полноту заполнения.
- Outcome в домене, поле outcome в сценарии, событие call.resolve у курсанта
и две кнопки на АРМ. Отдельное действие, а не «положил трубку»: система
должна отличить осознанное решение от брошенного вызова.
- Метрика outcome (E2, вес 3 — лишний выезд дороже неточного признака).
- Там, где карточка не заводится, метрики карточки не считаются вовсе.
Полнота опроса считается: передать вызов не значит не опрашивать.
- call.resolve останавливает норматив опроса, как и передача карточки.
Попутно (lct-34): библиотека стала вложенной — scenarios/tickets/, поля
ticket и position, чек-листы для медицины, полиции и ЖКХ. Перенесён билет 1
целиком: три вызова, три классификатора, третий — из другого региона.
Найдено по ходу: модификаторы списка оповещения не были подключены ни к чему.
В классификаторе скорая добавляется к массовой драке признаком «пострадавшие»,
но взять его было неоткуда. Теперь модификаторы берутся из карточки —
victims_count, life_threat, evacuation_needed, fire.gasified, — и список
оповещения пересобирается при правке любого из этих полей.
161 тест зелёный (13 новых), make typecheck чистый.
2026-09-20 08:33:30 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def modifiers(card: KIO) -> list[str]:
|
|
|
|
|
|
"""Какие условия оповещения включены обстоятельствами вызова."""
|
|
|
|
|
|
keys: list[str] = []
|
|
|
|
|
|
if card.victims_count:
|
|
|
|
|
|
keys.append("casualties")
|
|
|
|
|
|
if card.life_threat:
|
|
|
|
|
|
keys.append("life_threat")
|
|
|
|
|
|
if card.evacuation_needed:
|
|
|
|
|
|
keys.append("evacuation")
|
|
|
|
|
|
if card.fire is not None and card.fire.gasified:
|
|
|
|
|
|
keys.append("gas")
|
|
|
|
|
|
return keys
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
def derive_incident(card: KIO) -> KIO:
|
|
|
|
|
|
"""Пересчитать код происшествия и список оповещения по признакам.
|
|
|
|
|
|
|
|
|
|
|
|
Вызывается после каждой правки признаков: в боевом АРМ список оповещения
|
|
|
|
|
|
пересобирается сам, и это главное, чему учит карточка.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.domain import ekp
|
|
|
|
|
|
|
2026-09-21 20:10:57 +03:00
|
|
|
|
found = ekp.by_signs(card.signs, card.incident_group)
|
feat: исход вызова — не каждый звонок заканчивается карточкой (lct-36)
Весь продукт стоял на допущении «звонок → карточка → выезд». В билетах
заказчика оно нарушается намеренно: «поругался с продавцом Мегафон» — справка,
а вызов из Волгоградской области передаётся в систему-112 другого субъекта.
Курсант, заведший карточку на такой вызов, занял расчёт зря, и прежняя оценка
этого не видела — наоборот, награждала за полноту заполнения.
- Outcome в домене, поле outcome в сценарии, событие call.resolve у курсанта
и две кнопки на АРМ. Отдельное действие, а не «положил трубку»: система
должна отличить осознанное решение от брошенного вызова.
- Метрика outcome (E2, вес 3 — лишний выезд дороже неточного признака).
- Там, где карточка не заводится, метрики карточки не считаются вовсе.
Полнота опроса считается: передать вызов не значит не опрашивать.
- call.resolve останавливает норматив опроса, как и передача карточки.
Попутно (lct-34): библиотека стала вложенной — scenarios/tickets/, поля
ticket и position, чек-листы для медицины, полиции и ЖКХ. Перенесён билет 1
целиком: три вызова, три классификатора, третий — из другого региона.
Найдено по ходу: модификаторы списка оповещения не были подключены ни к чему.
В классификаторе скорая добавляется к массовой драке признаком «пострадавшие»,
но взять его было неоткуда. Теперь модификаторы берутся из карточки —
victims_count, life_threat, evacuation_needed, fire.gasified, — и список
оповещения пересобирается при правке любого из этих полей.
161 тест зелёный (13 новых), make typecheck чистый.
2026-09-20 08:33:30 +03:00
|
|
|
|
keys = modifiers(card)
|
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
|
|
|
|
data = card.model_dump()
|
|
|
|
|
|
data["incident_code"] = found.code if found else None
|
2026-09-21 17:40:54 +03:00
|
|
|
|
# `notify` недоступен оператору для записи (READ_ONLY_FIELDS), поэтому
|
|
|
|
|
|
# хранить прошлый авторасчёт как «добавленные вручную службы» нельзя:
|
|
|
|
|
|
# при снятии признака газа МОСГАЗ должен исчезнуть из нового расчёта.
|
|
|
|
|
|
data["notify"] = ekp.notify_list(found.code, keys) if found else []
|
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
|
|
|
|
return KIO.model_validate(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 43 события WS по шести союзам направлений,
таймеры ГОСТ, таксономия E1–E6, шесть компетенций радара. make types гоняет
их через JSON Schema в generated.ts, тест ловит забытую регенерацию.
Стенд: docker compose postgres + backend, health отвечает, база досягаема
из контейнера бэкенда. Makefile не врёт — нереализованные цели падают
и называют карточку, которая их закроет.
frontend/.npmrc: под WSL2 npm install зависает на дефолтных 15 сокетах.
2026-09-15 19:35:03 +03:00
|
|
|
|
def apply_patch(card: KIO, fields: dict[str, Any]) -> KIO:
|
|
|
|
|
|
"""Применить дельту `kio.patch`. Служебные поля игнорируются.
|
|
|
|
|
|
|
|
|
|
|
|
Вложенные поля приходят плоским путём: {"fire.floors": 5}.
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = card.model_dump()
|
|
|
|
|
|
for path, value in fields.items():
|
|
|
|
|
|
if path in READ_ONLY_FIELDS:
|
|
|
|
|
|
continue
|
|
|
|
|
|
head, _, tail = path.partition(".")
|
|
|
|
|
|
if not tail:
|
|
|
|
|
|
data[head] = value
|
|
|
|
|
|
continue
|
|
|
|
|
|
nested = data.get(head) or {}
|
|
|
|
|
|
nested[tail] = value
|
|
|
|
|
|
data[head] = nested
|
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
|
|
|
|
updated = KIO.model_validate(data)
|
feat: исход вызова — не каждый звонок заканчивается карточкой (lct-36)
Весь продукт стоял на допущении «звонок → карточка → выезд». В билетах
заказчика оно нарушается намеренно: «поругался с продавцом Мегафон» — справка,
а вызов из Волгоградской области передаётся в систему-112 другого субъекта.
Курсант, заведший карточку на такой вызов, занял расчёт зря, и прежняя оценка
этого не видела — наоборот, награждала за полноту заполнения.
- Outcome в домене, поле outcome в сценарии, событие call.resolve у курсанта
и две кнопки на АРМ. Отдельное действие, а не «положил трубку»: система
должна отличить осознанное решение от брошенного вызова.
- Метрика outcome (E2, вес 3 — лишний выезд дороже неточного признака).
- Там, где карточка не заводится, метрики карточки не считаются вовсе.
Полнота опроса считается: передать вызов не значит не опрашивать.
- call.resolve останавливает норматив опроса, как и передача карточки.
Попутно (lct-34): библиотека стала вложенной — scenarios/tickets/, поля
ticket и position, чек-листы для медицины, полиции и ЖКХ. Перенесён билет 1
целиком: три вызова, три классификатора, третий — из другого региона.
Найдено по ходу: модификаторы списка оповещения не были подключены ни к чему.
В классификаторе скорая добавляется к массовой драке признаком «пострадавшие»,
но взять его было неоткуда. Теперь модификаторы берутся из карточки —
victims_count, life_threat, evacuation_needed, fire.gasified, — и список
оповещения пересобирается при правке любого из этих полей.
161 тест зелёный (13 новых), make typecheck чистый.
2026-09-20 08:33:30 +03:00
|
|
|
|
# Пересчитываем не только на смену признаков: пострадавшие поднимают скорую,
|
|
|
|
|
|
# газификация — МОСГАЗ, и список оповещения обязан это отразить сразу.
|
|
|
|
|
|
touched = MODIFIER_FIELDS & set(fields)
|
|
|
|
|
|
return derive_incident(updated) if touched else updated
|