каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
"""Схемы событий WebSocket — источник истины для make types.
|
|
|
|
|
|
|
|
|
|
|
|
Контракт целиком: docs/arch/CONTRACT.md
|
|
|
|
|
|
Каналы: /ws/call (курсант), /ws/station (ДДС), /ws/observe (монитор и пульт,
|
|
|
|
|
|
только приём), /ws/control (только преподаватель, только передача).
|
|
|
|
|
|
|
|
|
|
|
|
Правила, зашитые в схемы:
|
|
|
|
|
|
* курсанту уходят дельты, наблюдателям — состояние целиком;
|
|
|
|
|
|
* все `at` — серверное UTC, фронт время не считает;
|
|
|
|
|
|
* ошибки идут одним каналом `error`, без HTTP-кодов внутри WS.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from enum import StrEnum
|
|
|
|
|
|
from typing import Annotated, Any, Literal
|
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
from pydantic import BaseModel, 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
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
from app.domain.classifiers import DDSCode, Level, Outcome
|
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
|
|
|
|
from app.domain.kio import KIO, Coords
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
from app.domain.roles import Role
|
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 чистый.
2026-09-20 08:23:55 +03:00
|
|
|
|
from app.domain.statuses import ServiceStatus, StationSnapshot
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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.taxonomy import Finding
|
|
|
|
|
|
from app.domain.timers import TimerSnapshot
|
2026-09-27 22:41:27 +03:00
|
|
|
|
from app.scoring.taxonomy import INSTRUCTOR_METRIC, METRIC_MAP
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 SessionMode(StrEnum):
|
|
|
|
|
|
"""Режим сессии. Меняет доступность подсказок и протоколирование,
|
|
|
|
|
|
но не поведение звонящего (docs/product/MODES.md)."""
|
|
|
|
|
|
|
|
|
|
|
|
TRAINING = "training"
|
|
|
|
|
|
EXAM = "exam"
|
|
|
|
|
|
SELF = "self"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 17:40:54 +03:00
|
|
|
|
class Exercise(StrEnum):
|
|
|
|
|
|
CALL = "call"
|
|
|
|
|
|
DDS = "dds"
|
|
|
|
|
|
CARD = "card"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
class LessonCriteria(BaseModel):
|
|
|
|
|
|
"""Настраиваемые преподавателем условия именно этого занятия.
|
|
|
|
|
|
|
|
|
|
|
|
Нормативы ГОСТ для приёма вызова сюда не входят. Для занятия меняются
|
2026-09-26 18:12:27 +03:00
|
|
|
|
учебные лимиты первичной реакции и полного цикла карточки ДДС, заполнения
|
|
|
|
|
|
КИО, порог успешности и веса метрик; веса сценария остаются базовыми,
|
|
|
|
|
|
настройки занятия могут их переопределить.
|
2026-09-24 01:10:49 +03:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
decision_time_limit_seconds: int = Field(default=30, ge=5, le=300)
|
2026-09-26 18:12:27 +03:00
|
|
|
|
card_fill_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
|
|
|
|
|
dds_card_work_time_limit_seconds: int = Field(default=180, ge=30, le=1800)
|
2026-09-27 16:26:10 +03:00
|
|
|
|
#: Сколько секунд после доклада бригады даётся на соответствующий статус.
|
2026-09-27 23:37:57 +03:00
|
|
|
|
dds_report_reaction_enabled: bool = False
|
2026-09-27 16:26:10 +03:00
|
|
|
|
dds_report_reaction_limit_seconds: int = Field(default=45, ge=5, le=600)
|
2026-09-24 01:10:49 +03:00
|
|
|
|
allowed_errors: int = Field(default=0, ge=0, le=50)
|
|
|
|
|
|
require_correct_grammar: bool = True
|
|
|
|
|
|
score_weights: dict[str, float] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("score_weights")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def validate_score_weights(cls, weights: dict[str, float]) -> dict[str, float]:
|
2026-09-27 22:41:27 +03:00
|
|
|
|
unknown = weights.keys() - METRIC_MAP.keys() - {INSTRUCTOR_METRIC}
|
2026-09-24 01:10:49 +03:00
|
|
|
|
if unknown:
|
|
|
|
|
|
raise ValueError(f"неизвестные метрики весов: {', '.join(sorted(unknown))}")
|
|
|
|
|
|
if any(not 0 <= weight <= 10 for weight in weights.values()):
|
|
|
|
|
|
raise ValueError("каждый вес метрики должен быть от 0 до 10")
|
|
|
|
|
|
return weights
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 Speaker(StrEnum):
|
|
|
|
|
|
CALLER = "caller"
|
|
|
|
|
|
OPERATOR = "operator"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Mood(StrEnum):
|
|
|
|
|
|
"""Состояние звонящего. Фон подачи, не факты."""
|
|
|
|
|
|
|
|
|
|
|
|
CALM = "calm"
|
|
|
|
|
|
WORRIED = "worried"
|
|
|
|
|
|
PANIC = "panic"
|
|
|
|
|
|
AGGRESSIVE = "aggressive"
|
|
|
|
|
|
CONFUSED = "confused"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallEndReason(StrEnum):
|
|
|
|
|
|
HANGUP = "hangup"
|
|
|
|
|
|
DROPPED = "dropped"
|
|
|
|
|
|
INSTRUCTOR = "instructor"
|
2026-09-21 17:40:54 +03:00
|
|
|
|
COMPLETE = "complete"
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 PatchSource(StrEnum):
|
|
|
|
|
|
"""`auto` — сервер распознал факт из речи, `operator` — эхо правки курсанта."""
|
|
|
|
|
|
|
|
|
|
|
|
AUTO = "auto"
|
|
|
|
|
|
OPERATOR = "operator"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DirectiveMode(StrEnum):
|
|
|
|
|
|
"""`immediate` — только для обрыва связи: рвёт TTS на полуслове."""
|
|
|
|
|
|
|
|
|
|
|
|
NEXT_TURN = "next_turn"
|
|
|
|
|
|
IMMEDIATE = "immediate"
|
|
|
|
|
|
|
|
|
|
|
|
|
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 ErrorKind(StrEnum):
|
|
|
|
|
|
"""Коды канала `error`. Фронт разбирает код, а не текст сообщения:
|
|
|
|
|
|
текст — для человека, код — для поведения интерфейса."""
|
|
|
|
|
|
|
|
|
|
|
|
SESSION_NOT_FOUND = "session_not_found"
|
|
|
|
|
|
CALL_NOT_STARTED = "call_not_started"
|
|
|
|
|
|
HINT_DENIED_IN_EXAM = "hint_denied_in_exam"
|
|
|
|
|
|
MODELS_WARMING_UP = "models_warming_up"
|
|
|
|
|
|
DIRECTIVE_NEEDS_NETWORK = "directive_needs_network"
|
|
|
|
|
|
SCENARIO_INVALID = "scenario_invalid"
|
|
|
|
|
|
UNSUPPORTED_EVENT = "unsupported_event"
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
#: Канал открыт не тем, кому он предназначен (lct-23). Фронт по этому коду
|
|
|
|
|
|
#: отправляет на вход, а не показывает «занятие не найдено».
|
|
|
|
|
|
FORBIDDEN = "forbidden"
|
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
|
|
|
|
INTERNAL = "internal"
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 TranscriptEntry(BaseModel):
|
|
|
|
|
|
"""Реплика в ленте. `ref` — якорь для пометок преподавателя и отметок разбора."""
|
|
|
|
|
|
|
|
|
|
|
|
ref: str
|
|
|
|
|
|
speaker: Speaker
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
mood: Mood | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────── сервер → курсант ───────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallIncoming(BaseModel):
|
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
|
|
|
|
"""Обязательность полей приходит сценарием, а не моделью: без `required_fields`
|
|
|
|
|
|
АРМ не может подсветить незаполненное обязательное поле, и курсант узнаёт
|
|
|
|
|
|
о неполноте карточки только из разбора."""
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
type: Literal["call.incoming"] = "call.incoming"
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
caller_number: str
|
|
|
|
|
|
level: Level
|
|
|
|
|
|
mode: SessionMode
|
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
|
|
|
|
required_fields: list[str] = []
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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-21 17:40:54 +03:00
|
|
|
|
class CardBriefing(BaseModel):
|
|
|
|
|
|
"""Текстовая вводная упражнения 112; эталонные значения сюда не входят."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["card.briefing"] = "card.briefing"
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
mode: SessionMode
|
|
|
|
|
|
text: str
|
|
|
|
|
|
required_fields: list[str]
|
|
|
|
|
|
card: KIO | None = None
|
2026-09-21 20:10:57 +03:00
|
|
|
|
handoff_to_dds: bool = False
|
2026-09-21 17:40:54 +03:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
class TextTurnAccepted(BaseModel):
|
|
|
|
|
|
type: Literal["text.turn.accepted"] = "text.turn.accepted"
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 CallStarted(BaseModel):
|
|
|
|
|
|
type: Literal["call.started"] = "call.started"
|
|
|
|
|
|
started_at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SttPartial(BaseModel):
|
|
|
|
|
|
type: Literal["stt.partial"] = "stt.partial"
|
|
|
|
|
|
text: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SttFinal(BaseModel):
|
|
|
|
|
|
type: Literal["stt.final"] = "stt.final"
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallerUtterance(BaseModel):
|
|
|
|
|
|
type: Literal["caller.utterance"] = "caller.utterance"
|
|
|
|
|
|
utterance_id: UUID
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
mood: Mood
|
2026-09-26 18:12:27 +03:00
|
|
|
|
source: Literal["local_llm", "scenario"] = "scenario"
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 TtsBegin(BaseModel):
|
|
|
|
|
|
type: Literal["tts.begin"] = "tts.begin"
|
|
|
|
|
|
utterance_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TtsEnd(BaseModel):
|
|
|
|
|
|
type: Literal["tts.end"] = "tts.end"
|
|
|
|
|
|
utterance_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TtsCancel(BaseModel):
|
|
|
|
|
|
"""Оператор перебил. Фронт мгновенно чистит очередь воспроизведения."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["tts.cancel"] = "tts.cancel"
|
|
|
|
|
|
utterance_id: UUID
|
|
|
|
|
|
reason: Literal["barge_in", "director"] = "barge_in"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BgStart(BaseModel):
|
|
|
|
|
|
"""Аудио-фон происшествия. Файл лежит в сборке фронта, трафика фон не создаёт."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["bg.start"] = "bg.start"
|
|
|
|
|
|
loop: str
|
|
|
|
|
|
gain_db: float
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BgStop(BaseModel):
|
|
|
|
|
|
type: Literal["bg.stop"] = "bg.stop"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EraData(BaseModel):
|
|
|
|
|
|
"""Автоданные ЭРА-ГЛОНАСС до соединения с водителем."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["era.data"] = "era.data"
|
|
|
|
|
|
vin: str
|
|
|
|
|
|
coords: Coords
|
|
|
|
|
|
passengers: int
|
|
|
|
|
|
impact_force: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KioPatchOut(BaseModel):
|
|
|
|
|
|
"""Сервер распознал факт из речи либо шлёт эхо чужой правки."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["kio.patch"] = "kio.patch"
|
|
|
|
|
|
fields: dict[str, Any]
|
|
|
|
|
|
source: PatchSource
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HintShown(BaseModel):
|
|
|
|
|
|
type: Literal["hint.shown"] = "hint.shown"
|
|
|
|
|
|
checklist_id: str
|
|
|
|
|
|
question: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimerTick(BaseModel):
|
|
|
|
|
|
"""Раз в секунду, не на каждое изменение: таймеров дюжина, UI рисует секунды."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["timer.tick"] = "timer.tick"
|
|
|
|
|
|
timers: list[TimerSnapshot]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallEnded(BaseModel):
|
|
|
|
|
|
type: Literal["call.ended"] = "call.ended"
|
|
|
|
|
|
reason: CallEndReason
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ScoreReady(BaseModel):
|
|
|
|
|
|
type: Literal["score.ready"] = "score.ready"
|
|
|
|
|
|
session_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
class CommandAck(BaseModel):
|
|
|
|
|
|
"""Durable confirmation for a station command, safe to replay by ID."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["command.ack"] = "command.ack"
|
|
|
|
|
|
command_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 ErrorEvent(BaseModel):
|
|
|
|
|
|
type: Literal["error"] = "error"
|
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
|
|
|
|
code: ErrorKind
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
message: str
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-27 16:35:13 +03:00
|
|
|
|
class SessionPaused(BaseModel):
|
|
|
|
|
|
"""Пульт остановил или возобновил время занятия (lct-39)."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["session.paused"] = "session.paused"
|
|
|
|
|
|
paused: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
ServerToTrainee = Annotated[
|
|
|
|
|
|
CallIncoming
|
2026-09-21 17:40:54 +03:00
|
|
|
|
| CardBriefing
|
2026-09-26 18:12:27 +03:00
|
|
|
|
| TextTurnAccepted
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
| CallStarted
|
|
|
|
|
|
| SttPartial
|
|
|
|
|
|
| SttFinal
|
|
|
|
|
|
| CallerUtterance
|
|
|
|
|
|
| TtsBegin
|
|
|
|
|
|
| TtsEnd
|
|
|
|
|
|
| TtsCancel
|
|
|
|
|
|
| BgStart
|
|
|
|
|
|
| BgStop
|
|
|
|
|
|
| EraData
|
|
|
|
|
|
| KioPatchOut
|
|
|
|
|
|
| HintShown
|
|
|
|
|
|
| TimerTick
|
|
|
|
|
|
| CallEnded
|
|
|
|
|
|
| ScoreReady
|
2026-09-27 16:35:13 +03:00
|
|
|
|
| SessionPaused
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
| ErrorEvent,
|
|
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────── курсант → сервер ───────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallAnswer(BaseModel):
|
|
|
|
|
|
"""Снял гарнитуру. Останавливает норматив `answer`."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["call.answer"] = "call.answer"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 17:40:54 +03:00
|
|
|
|
class CardSubmit(BaseModel):
|
|
|
|
|
|
type: Literal["card.submit"] = "card.submit"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
class TextTurn(BaseModel):
|
|
|
|
|
|
type: Literal["text.turn"] = "text.turn"
|
|
|
|
|
|
text: str = Field(min_length=1, max_length=1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 KioPatchIn(BaseModel):
|
|
|
|
|
|
"""Правка карточки. Дебаунс 300 мс, шлётся только дельта."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["kio.patch"] = "kio.patch"
|
|
|
|
|
|
fields: dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HintRequest(BaseModel):
|
|
|
|
|
|
"""В режиме `exam` сервер отвечает событием `error`."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["hint.request"] = "hint.request"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SelfAssessmentSubmit(BaseModel):
|
|
|
|
|
|
"""Самооценка до показа автооценки."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["self_assessment.submit"] = "self_assessment.submit"
|
|
|
|
|
|
missed: list[str]
|
|
|
|
|
|
comment: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DdsDispatch(BaseModel):
|
2026-09-21 17:40:54 +03:00
|
|
|
|
"""Передача в ДДС. При маршрутизации по ЕКП служба уже есть в notify."""
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
|
|
|
|
|
|
type: Literal["dds.dispatch"] = "dds.dispatch"
|
2026-09-21 17:40:54 +03:00
|
|
|
|
service: DDSCode | 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallHangup(BaseModel):
|
|
|
|
|
|
type: Literal["call.hangup"] = "call.hangup"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CallbackDial(BaseModel):
|
|
|
|
|
|
"""Обратный дозвон после обрыва: 3 попытки по 10 с."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["callback.dial"] = "callback.dial"
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
class CallResolve(BaseModel):
|
|
|
|
|
|
"""Курсант решил, что карточка здесь не заводится.
|
|
|
|
|
|
|
|
|
|
|
|
Не «завершить звонок», а именно «этот вызов закрывается иначе»: справкой
|
|
|
|
|
|
или передачей в другой регион. Решение оценивается наравне с выбором
|
|
|
|
|
|
признаков — это та же классификация, только на шаг раньше.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["call.resolve"] = "call.resolve"
|
|
|
|
|
|
outcome: Outcome
|
|
|
|
|
|
comment: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
TraineeToServer = Annotated[
|
|
|
|
|
|
CallAnswer
|
2026-09-21 17:40:54 +03:00
|
|
|
|
| CardSubmit
|
2026-09-26 18:12:27 +03:00
|
|
|
|
| TextTurn
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
| KioPatchIn
|
|
|
|
|
|
| HintRequest
|
|
|
|
|
|
| SelfAssessmentSubmit
|
|
|
|
|
|
| DdsDispatch
|
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
|
|
|
|
| CallResolve
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
| CallHangup
|
|
|
|
|
|
| CallbackDial,
|
|
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ───────────────────────── сервер → наблюдателям ─────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionSnapshot(BaseModel):
|
|
|
|
|
|
"""Обязательно при подключении: монитор в классе включают посреди занятия."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["session.snapshot"] = "session.snapshot"
|
|
|
|
|
|
session_id: UUID
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
scenario_title: str
|
|
|
|
|
|
level: Level
|
|
|
|
|
|
mode: SessionMode
|
2026-09-21 17:40:54 +03:00
|
|
|
|
exercise: Exercise = Exercise.CALL
|
2026-09-24 01:10:49 +03:00
|
|
|
|
criteria: LessonCriteria = Field(default_factory=LessonCriteria)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
trainee_name: str | None = None
|
|
|
|
|
|
started_at: datetime | None = None
|
|
|
|
|
|
kio: KIO
|
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
|
|
|
|
required_fields: list[str] = []
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
transcript: list[TranscriptEntry]
|
|
|
|
|
|
timers: list[TimerSnapshot]
|
|
|
|
|
|
hints_used: int = 0
|
|
|
|
|
|
ended: bool = False
|
2026-09-27 16:35:13 +03:00
|
|
|
|
#: Пульт остановил время занятия (lct-39).
|
|
|
|
|
|
paused: bool = False
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 TranscriptAppend(BaseModel):
|
|
|
|
|
|
type: Literal["transcript.append"] = "transcript.append"
|
|
|
|
|
|
entry: TranscriptEntry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KioState(BaseModel):
|
|
|
|
|
|
"""Полная карточка, не дельта: наблюдателю проще, рассинхрон дороже килобайт."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["kio.state"] = "kio.state"
|
|
|
|
|
|
kio: KIO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ModeSet(BaseModel):
|
|
|
|
|
|
type: Literal["mode.set"] = "mode.set"
|
|
|
|
|
|
mode: SessionMode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionEnded(BaseModel):
|
|
|
|
|
|
type: Literal["session.ended"] = "session.ended"
|
|
|
|
|
|
reason: CallEndReason
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InstructorNoteShown(BaseModel):
|
|
|
|
|
|
"""Пометка преподавателя видна всем, кому виден транскрипт."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["instructor_note.shown"] = "instructor_note.shown"
|
|
|
|
|
|
transcript_ref: str
|
|
|
|
|
|
text: str
|
|
|
|
|
|
author: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReferenceStarted(BaseModel):
|
|
|
|
|
|
"""Автопроигрывание эталонного звонка на внешнем мониторе."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["reference.started"] = "reference.started"
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ServerToObserver = Annotated[
|
|
|
|
|
|
SessionSnapshot
|
|
|
|
|
|
| TranscriptAppend
|
|
|
|
|
|
| KioState
|
|
|
|
|
|
| TimerTick
|
|
|
|
|
|
| ModeSet
|
|
|
|
|
|
| HintShown
|
|
|
|
|
|
| BgStart
|
|
|
|
|
|
| BgStop
|
|
|
|
|
|
| CallerUtterance
|
|
|
|
|
|
| SessionEnded
|
|
|
|
|
|
| ScoreReady
|
|
|
|
|
|
| InstructorNoteShown
|
|
|
|
|
|
| ReferenceStarted
|
2026-09-27 16:35:13 +03:00
|
|
|
|
| SessionPaused
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
| ErrorEvent,
|
|
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ──────────────────── преподаватель → сервер (control) ────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ScenarioStart(BaseModel):
|
|
|
|
|
|
type: Literal["scenario.start"] = "scenario.start"
|
|
|
|
|
|
scenario_id: str
|
2026-09-21 19:56:13 +03:00
|
|
|
|
scenario_ids: list[str] | None = None
|
2026-09-26 18:12:27 +03:00
|
|
|
|
random_scenario_ids: list[str] | None = None
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# Pace defaults to simultaneous for older clients; the instructor UI
|
|
|
|
|
|
# explicitly sends its slower training default.
|
|
|
|
|
|
dds_arrival_interval_seconds: int = Field(default=0, ge=0, le=300)
|
|
|
|
|
|
dds_max_waiting: int = Field(default=3, ge=1, le=10)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
trainee: str
|
2026-09-21 17:40:54 +03:00
|
|
|
|
trainee_id: UUID | None = None
|
2026-09-24 01:10:49 +03:00
|
|
|
|
# Служба обучающегося определяет, чьи статусы он ведёт. В рабочем режиме
|
|
|
|
|
|
# сервер заменяет это значение данными учётной записи; поле нужно demo без БД.
|
|
|
|
|
|
dds_service: 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
|
|
|
|
group_id: str | None = None
|
|
|
|
|
|
mode: SessionMode
|
2026-09-21 17:40:54 +03:00
|
|
|
|
exercise: Exercise = Exercise.CALL
|
2026-09-21 20:10:57 +03:00
|
|
|
|
handoff_to_dds: bool = False
|
2026-09-24 01:10:49 +03:00
|
|
|
|
criteria: LessonCriteria = Field(default_factory=LessonCriteria)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 DirectorInject(BaseModel):
|
|
|
|
|
|
"""Директива звонящему. Карточку курсанта не трогает ни одна команда."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["director.inject"] = "director.inject"
|
|
|
|
|
|
directive: str
|
|
|
|
|
|
mode: DirectiveMode = DirectiveMode.NEXT_TURN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ReferencePlay(BaseModel):
|
|
|
|
|
|
type: Literal["reference.play"] = "reference.play"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InstructorNoteAdd(BaseModel):
|
|
|
|
|
|
type: Literal["instructor_note.add"] = "instructor_note.add"
|
|
|
|
|
|
transcript_ref: str
|
|
|
|
|
|
text: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ScoreOverride(BaseModel):
|
|
|
|
|
|
"""Коррекция оценки. Автооценка сохраняется рядом."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["score.override"] = "score.override"
|
|
|
|
|
|
session_id: UUID
|
|
|
|
|
|
verdict: str
|
|
|
|
|
|
comment: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionStop(BaseModel):
|
|
|
|
|
|
type: Literal["session.stop"] = "session.stop"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-27 16:35:13 +03:00
|
|
|
|
class SessionPause(BaseModel):
|
|
|
|
|
|
"""Остановить время занятия; карточку курсанта команда не меняет (lct-39)."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["session.pause"] = "session.pause"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionResume(BaseModel):
|
|
|
|
|
|
type: Literal["session.resume"] = "session.resume"
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
InstructorToServer = Annotated[
|
|
|
|
|
|
ScenarioStart
|
|
|
|
|
|
| DirectorInject
|
|
|
|
|
|
| ReferencePlay
|
|
|
|
|
|
| InstructorNoteAdd
|
|
|
|
|
|
| ScoreOverride
|
2026-09-27 16:35:13 +03:00
|
|
|
|
| SessionStop
|
|
|
|
|
|
| SessionPause
|
|
|
|
|
|
| SessionResume,
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ───────────────────────────── станция ДДС ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CardReceived(BaseModel):
|
|
|
|
|
|
"""Снимок КИО. После передачи не меняется — оператор не дописывает задним числом."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["card.received"] = "card.received"
|
|
|
|
|
|
card: KIO
|
|
|
|
|
|
from_operator: str
|
|
|
|
|
|
at: datetime
|
2026-09-21 19:56:13 +03:00
|
|
|
|
card_index: int = 1
|
|
|
|
|
|
card_total: int = 1
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 CardAck(BaseModel):
|
docs: датасет заказчика — разбор, расшифровка билетов, ревизия карточек
Получены первичные данные, названные в ТЗ: памятка по АРМ-112, классификатор
происшествий на 1283 кода и 32 экзаменационных билета. Файлы в docs/spec/source/
под гитом: документ, на который ссылается норматив, должен приезжать вместе с кодом.
Что изменилось по существу:
- Норматив подтверждения карточки был 4 секунды со ссылкой на ГОСТ, где этого числа
нет. Памятка и ТЗ независимо называют 30 секунд — исправлено в domain/timers.py
с правильной ссылкой (ПП РФ № 1931), в контракте и в generated.ts.
- Оператор в боевом АРМ-112 службу не выбирает: он проставляет признаки, а список
оповещения из 5–11 служб считается по ЕКП. Наше поле dds моделирует не ту работу.
- Сторона ДДС описана памяткой подробнее, чем реализована: девять статусов
реагирования автоматом, семь статусов карточки, обязательные комментарии
к отказам и таксономия ошибок с примерами.
- 96 учебных вызовов из билетов расшифрованы слово в слово в docs/spec/TICKETS.md;
сложность в них делается адресом и профильностью, а не сюжетом.
Документы: DATASET.md, TICKETS.md, правки NORMATIVES.md (НПА, ЕКП, статусы),
GAP.md (пункты 13–15), TZ.md (предположение «датасета не будет» не подтвердилось).
Карточки: новые 32–36 (классификатор, статусы реагирования, билеты, уточнение
адреса, непрофильные вызовы); в девятнадцати существующих — раздел «Датасет»
с расхождениями, включая выполненные: lct-01 моделирует не ту карточку КИО,
lct-12 сравнивает адрес так, что уточнивший его курсант получает расхождение
с эталоном, lct-10 не содержит трёх блоков боевого АРМ.
2026-09-19 19:42:48 +03:00
|
|
|
|
"""Останавливает норматив `dds_ack` (≤ 30 с)."""
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
|
|
|
|
|
|
type: Literal["card.ack"] = "card.ack"
|
2026-09-26 18:12:27 +03:00
|
|
|
|
comment: str = Field(min_length=1, max_length=1000)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 CardBounce(BaseModel):
|
|
|
|
|
|
"""Возврат на уточнение: неполнота КИО становится сорванным выездом."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["card.bounce"] = "card.bounce"
|
|
|
|
|
|
missing_fields: list[str]
|
|
|
|
|
|
comment: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
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 чистый.
2026-09-20 08:23:55 +03:00
|
|
|
|
class ServiceStatusSet(BaseModel):
|
|
|
|
|
|
"""Диспетчер ставит статус реагирования своей службе.
|
|
|
|
|
|
|
|
|
|
|
|
Последовательность жёсткая, комментарий к отказу обязателен — это не
|
|
|
|
|
|
валидация формы, а то, чему учит второй режим занятия
|
|
|
|
|
|
(docs/spec/DATASET.md#статусы-реагирования).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["card.status"] = "card.status"
|
|
|
|
|
|
service: str
|
|
|
|
|
|
status: ServiceStatus
|
|
|
|
|
|
comment: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 17:40:54 +03:00
|
|
|
|
class CrewSelect(BaseModel):
|
|
|
|
|
|
type: Literal["crew.select"] = "crew.select"
|
|
|
|
|
|
crew: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PhoneDial(BaseModel):
|
|
|
|
|
|
type: Literal["phone.dial"] = "phone.dial"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
class PhoneBrief(BaseModel):
|
|
|
|
|
|
"""Что диспетчер передал старшему группы при первом исходящем звонке."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["phone.brief"] = "phone.brief"
|
|
|
|
|
|
address: str = Field(min_length=3, max_length=300)
|
|
|
|
|
|
incident: str = Field(min_length=3, max_length=500)
|
|
|
|
|
|
request: str = Field(default="", max_length=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PhoneCheck(BaseModel):
|
|
|
|
|
|
"""Запрос диспетчера об обстановке после предыдущего доклада."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["phone.check"] = "phone.check"
|
|
|
|
|
|
text: str = Field(min_length=8, max_length=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PhoneHangup(BaseModel):
|
|
|
|
|
|
type: Literal["phone.hangup"] = "phone.hangup"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PhoneLine(BaseModel):
|
|
|
|
|
|
type: Literal["phone.line"] = "phone.line"
|
|
|
|
|
|
service: str
|
|
|
|
|
|
crew: str
|
|
|
|
|
|
speaker: Literal["dispatcher", "crew"]
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 17:40:54 +03:00
|
|
|
|
class PhoneReport(BaseModel):
|
|
|
|
|
|
type: Literal["phone.report"] = "phone.report"
|
|
|
|
|
|
service: str
|
|
|
|
|
|
crew: str
|
|
|
|
|
|
phase: Literal["dispatched", "arrived", "working", "completed"]
|
|
|
|
|
|
text: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StationFinish(BaseModel):
|
|
|
|
|
|
type: Literal["station.finish"] = "station.finish"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 19:56:13 +03:00
|
|
|
|
class CardReply(BaseModel):
|
|
|
|
|
|
type: Literal["card.reply"] = "card.reply"
|
|
|
|
|
|
card_id: UUID
|
|
|
|
|
|
text: str = Field(max_length=2000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CardNext(BaseModel):
|
|
|
|
|
|
type: Literal["card.next"] = "card.next"
|
|
|
|
|
|
card_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-24 01:10:49 +03:00
|
|
|
|
class CardOpen(BaseModel):
|
|
|
|
|
|
"""Открыть одну из уже выданных карточек, не меняя её таймер."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["card.open"] = "card.open"
|
|
|
|
|
|
card_id: UUID
|
|
|
|
|
|
|
|
|
|
|
|
|
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 чистый.
2026-09-20 08:23:55 +03:00
|
|
|
|
class StationState(BaseModel):
|
|
|
|
|
|
"""Состояние АРМ ДДС после каждой отметки: что стоит и что доступно дальше."""
|
|
|
|
|
|
|
|
|
|
|
|
type: Literal["station.state"] = "station.state"
|
|
|
|
|
|
snapshot: StationSnapshot
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 ZoneDecision(BaseModel):
|
|
|
|
|
|
type: Literal["zone.decision"] = "zone.decision"
|
|
|
|
|
|
in_zone: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CrewDispatched(BaseModel):
|
|
|
|
|
|
type: Literal["crew.dispatched"] = "crew.dispatched"
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CrewArrived(BaseModel):
|
|
|
|
|
|
type: Literal["crew.arrived"] = "crew.arrived"
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ServerToStation = Annotated[
|
2026-09-26 18:12:27 +03:00
|
|
|
|
CardReceived | StationState | PhoneLine | PhoneReport | TimerTick | SessionEnded
|
2026-09-27 16:35:13 +03:00
|
|
|
|
| ScoreReady | CommandAck | SessionPaused | ErrorEvent,
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
StationToServer = Annotated[
|
2026-09-24 01:10:49 +03:00
|
|
|
|
CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck
|
|
|
|
|
|
| PhoneHangup | StationFinish
|
|
|
|
|
|
| CardReply | CardNext | CardOpen
|
2026-09-21 17:40:54 +03:00
|
|
|
|
| ZoneDecision | CrewDispatched | CrewArrived,
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
Field(discriminator="type"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────── отчёт по HTTP ───────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Metric(BaseModel):
|
2026-09-26 18:12:27 +03:00
|
|
|
|
"""Факт против норматива со ссылкой; `credit` задаёт частичный вклад времени."""
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
|
|
|
|
|
|
key: str
|
|
|
|
|
|
title: str
|
|
|
|
|
|
fact: str
|
|
|
|
|
|
norm: str
|
|
|
|
|
|
ref: str | None = None
|
|
|
|
|
|
passed: bool
|
|
|
|
|
|
weight: float = 1.0
|
2026-09-26 18:12:27 +03:00
|
|
|
|
credit: float | None = Field(default=None, ge=0, le=1)
|
2026-09-27 16:13:21 +03:00
|
|
|
|
#: Номер карточки очереди ДДС (с 1) и служба — область метрики для разбора.
|
|
|
|
|
|
card: int | None = None
|
|
|
|
|
|
service: str | None = None
|
|
|
|
|
|
#: Автоматический вердикт, если его изменило решение преподавателя по отметке.
|
|
|
|
|
|
passed_auto: bool | None = None
|
|
|
|
|
|
credit_auto: float | None = Field(default=None, ge=0, le=1)
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 CompetencyScore(BaseModel):
|
|
|
|
|
|
competency: str
|
|
|
|
|
|
value: float
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-26 18:12:27 +03:00
|
|
|
|
class AIRecommendation(BaseModel):
|
|
|
|
|
|
metric_key: str
|
|
|
|
|
|
text: str = Field(min_length=12, max_length=240)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AICoaching(BaseModel):
|
|
|
|
|
|
status: Literal["ready", "unavailable", "disabled", "not_needed"]
|
|
|
|
|
|
model: str | None = None
|
|
|
|
|
|
recommendations: list[AIRecommendation] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 HintUsage(BaseModel):
|
|
|
|
|
|
checklist_id: str
|
|
|
|
|
|
question: str
|
|
|
|
|
|
at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SelfAssessment(BaseModel):
|
|
|
|
|
|
missed: list[str]
|
|
|
|
|
|
comment: str = ""
|
|
|
|
|
|
submitted_at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон
Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.
Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.
Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.
Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.
Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
|
|
|
|
class SelfAssessmentDiff(BaseModel):
|
|
|
|
|
|
"""Расхождение самооценки с автооценкой — отдельный материал для преподавателя.
|
|
|
|
|
|
|
|
|
|
|
|
Курсант, не заметивший, что пропустил вопрос о пострадавших, — более важный
|
|
|
|
|
|
случай, чем сама ошибка (docs/product/DEBRIEF.md).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
#: Пропустил и сам это заметил.
|
|
|
|
|
|
noticed: list[str] = []
|
|
|
|
|
|
#: Пропустил и не заметил — самое ценное для разбора.
|
|
|
|
|
|
unnoticed: list[str] = []
|
|
|
|
|
|
#: Отметил как пропущенное, хотя на деле спросил.
|
|
|
|
|
|
overcautious: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-21 19:56:13 +03:00
|
|
|
|
class DdsCardReport(BaseModel):
|
|
|
|
|
|
card_id: UUID
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
score_auto: float
|
2026-09-27 22:41:27 +03:00
|
|
|
|
#: Балл карточки после решений преподавателя по отметкам; None — решений не было.
|
|
|
|
|
|
score_reviewed: float | None = None
|
2026-09-21 19:56:13 +03:00
|
|
|
|
reply_text: str
|
|
|
|
|
|
metrics: list[Metric]
|
|
|
|
|
|
findings: list[Finding]
|
|
|
|
|
|
actions: list[dict[str, Any]] = []
|
|
|
|
|
|
duration_ms: int = 0
|
2026-09-26 18:12:27 +03:00
|
|
|
|
title: str | None = None
|
|
|
|
|
|
address: str | None = None
|
|
|
|
|
|
description: str | None = None
|
|
|
|
|
|
incident_type: str | None = None
|
|
|
|
|
|
victims_count: int | None = None
|
|
|
|
|
|
received_at: datetime | None = None
|
|
|
|
|
|
managed_service: str | None = None
|
|
|
|
|
|
recipient_services: list[str] = []
|
2026-09-21 19:56:13 +03:00
|
|
|
|
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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 SessionReport(BaseModel):
|
|
|
|
|
|
"""Единица истории: из отчётов складываются профиль, дельта попыток, аналитика."""
|
|
|
|
|
|
|
|
|
|
|
|
session_id: UUID
|
|
|
|
|
|
scenario_id: str
|
|
|
|
|
|
mode: SessionMode
|
2026-09-26 18:12:27 +03:00
|
|
|
|
exercise: Exercise = Exercise.CALL
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
attempt: int = 1
|
2026-09-24 01:10:49 +03:00
|
|
|
|
criteria: LessonCriteria
|
|
|
|
|
|
failed_metrics: int
|
|
|
|
|
|
passed: bool
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
transcript: list[TranscriptEntry]
|
|
|
|
|
|
findings: list[Finding]
|
|
|
|
|
|
metrics: list[Metric]
|
2026-09-26 18:12:27 +03:00
|
|
|
|
ai_coaching: AICoaching | None = None
|
2026-09-21 19:56:13 +03:00
|
|
|
|
card_results: list[DdsCardReport] = []
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
competencies: list[CompetencyScore]
|
|
|
|
|
|
reference_questions: list[HintShown]
|
|
|
|
|
|
missed_checklist: list[str]
|
|
|
|
|
|
hints_used: list[HintUsage]
|
|
|
|
|
|
self_assessment: SelfAssessment | None = None
|
lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон
Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.
Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.
Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.
Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.
Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
|
|
|
|
self_assessment_diff: SelfAssessmentDiff | None = None
|
|
|
|
|
|
notes: list[InstructorNoteShown] = []
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
score_auto: float
|
2026-09-27 16:13:21 +03:00
|
|
|
|
#: Балл после решений преподавателя по отметкам; None — решений не было.
|
|
|
|
|
|
score_reviewed: float | 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
|
|
|
|
score_final: float
|
|
|
|
|
|
overridden_by: str | None = None
|
|
|
|
|
|
override_comment: str | None = None
|
2026-09-27 16:35:13 +03:00
|
|
|
|
#: Суммарная длительность пауз преподавателя — время объяснимо (lct-39).
|
|
|
|
|
|
total_paused_ms: int = 0
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
class Principal(BaseModel):
|
|
|
|
|
|
"""Кто вошёл. В событиях не участвует, но фронт разводит по роли экраны,
|
|
|
|
|
|
и список ролей должен приезжать из домена, а не переписываться руками
|
|
|
|
|
|
(lct-23). Полная модель — в `app/api/auth.py`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
login: str
|
|
|
|
|
|
full_name: str
|
|
|
|
|
|
role: Role
|
|
|
|
|
|
service: str | None = None
|
|
|
|
|
|
trainee_id: UUID | 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
|
|
|
|
class EventCatalog(BaseModel):
|
|
|
|
|
|
"""Единственное назначение — собрать все союзы в одну JSON Schema для make types."""
|
|
|
|
|
|
|
feat: вход, роли и аудит действий (lct-23)
Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались
ссылкой с номером занятия, и пускало знание адреса.
- Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная
cookie; роль на сокетах читается из той же cookie в момент рукопожатия,
отдельного протокола авторизации в канале нет.
- Разграничение: control — преподавателю, observe — преподавателю и админу,
call и station — обучающемуся и преподавателю. Отказ приходит событием
error с кодом forbidden.
- Обучающийся не видит чужого: история подменяет фильтр на его собственный
идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ
к чужим результатам, а не только к чужим экранам.
- Администратору закрыта правка оценок — ТЗ запрещает это прямо.
- make users заводит по записи на роль и печатает случайные пароли один раз:
зашитый в репозиторий admin/admin пережил бы сдачу.
- Экран входа и проверка роли на каждом маршруте фронта.
Наши инструменты не сломались: make lesson и тесты входят через dev-token
за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной
функции не должно быть видно вовсе. У тестов появился conftest.py.
Role уехала в домен и в generated.ts через EventCatalog.principal: иначе
фронт переписывал бы список ролей руками.
181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
|
|
|
|
principal: Principal
|
|
|
|
|
|
|
каркас: домен, генерация типов фронта, стенд и карточки задач
Контракт в коде: КИО по нормативу, 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
|
|
|
|
server_to_trainee: ServerToTrainee
|
|
|
|
|
|
trainee_to_server: TraineeToServer
|
|
|
|
|
|
server_to_observer: ServerToObserver
|
|
|
|
|
|
instructor_to_server: InstructorToServer
|
|
|
|
|
|
server_to_station: ServerToStation
|
|
|
|
|
|
station_to_server: StationToServer
|
|
|
|
|
|
session_report: SessionReport
|