Commit graph

77 commits

Author SHA1 Message Date
GGlamer
d44dbc8d92 test: таймаут разовой проверки, кэш отказа, запись версии узла в БД при сверке и входе 2026-09-27 22:28:02 +03:00
GGlamer
a8d717200d fix: разовая проверка логина с таймаутом и кэшем отказа, версия узла выше БД записывается в БД 2026-09-27 22:25:01 +03:00
kaifarikman
a35aa3f6a2 fix: защитить вход на втором узле от гонок 2026-09-27 18:11:51 +03:00
kaifarikman
42f1da7f83 fix: разовая проверка не затирает более свежий локальный отзыв
lct-42: стейл-ответ SELECT, пришедший после конкурентного invalidate_login
на том же узле, откатывал поколение назад и снова принимал отозванную
cookie. После ожидания перепроверяем кэш и не перезаписываем более свежее
значение. Детерминированный тест гонки; SCALE-OUT.md отделяет старые
замеры от непроверенного кластерным смоуком изменения.
2026-09-27 17:13:01 +03:00
kaifarikman
b6617d7df8 fix: неизвестный узлу логин проверяется разово, не отзывается сразу
lct-42: узел кластера принимал вход другого узла первую секунду за отзыв
и рвал сокет FORBIDDEN. Неизвестный логин теперь проверяется разовым
SELECT auth_version, результат кэшируется, параллельные запросы одного
логина дедуплицируются; логина нет в users или БД недоступна — как раньше.
2026-09-27 17:05:29 +03:00
Ivan Gerasimov
aa6860fc6c test: тесты не зависят от LLM-настроек личного .env и сети WSL2 2026-09-27 15:35:21 +03:00
Ivan Gerasimov
a2ce08f46a fix: отозванная cookie не оживает, когда логина нет в users 2026-09-27 15:35:20 +03:00
GGlamer
b4420cea16 docs: README для жюри со скриншотами экранов 2026-09-27 13:20:56 +03:00
gglamer
9f2ef7691f refactor: оценка занятия по неизменяемому снимку, грамматика параметром, тесты без SimpleNamespace 2026-09-27 08:06:05 +00:00
gglamer
a11206527a refactor: одна функция метрики норматива времени для карточки 112 и карточки ДДС 2026-09-27 08:06:05 +00:00
gglamer
36a9862a94 refactor: общий вход в сокет занятия и выполнение команды с fencing вместо копий в четырёх каналах 2026-09-27 07:56:12 +00:00
gglamer
f9b6fafb7c refactor: завершение занятия — одна операция end_session вместо четырёх копий в сокетах 2026-09-27 07:40:31 +00:00
gglamer
0d7bbf39b3 refactor: lease занятий ведёт хаб поверх SessionStore, main и admin не читают реестр хаба напрямую 2026-09-26 23:17:04 +00:00
gglamer
47cb85ee02 refactor: запись хода занятия — один commit SessionStore на операцию вместо журнала и checkpoint
Правка балла с пульта и из отчёта идёт одной доменной операцией: раньше WS-путь не обновлял full_report живой сессии.
2026-09-26 22:48:24 +00:00
gglamer
72c6a7cfa3 refactor: снимок занятия — model_dump сохраняемой части сессии вместо ручного перечисления полей 2026-09-26 22:13:35 +00:00
gglamer
64313a62fb refactor: прогресс звонящего вынесен в модели SlotProgress и PersonaProgress 2026-09-26 22:13:35 +00:00
gglamer
fca353fdd0 refactor: правила пульта ДДС перенесены в DdsDesk.apply, сокет станции — только транспорт 2026-09-26 22:01:27 +00:00
gglamer
5a3761a830 refactor: пульт ДДС владеет живыми карточками, сессия не копирует их поля
Связка 112→ДДС больше не подменяет таймеры оператора таймерами первой
карточки ДДС, поэтому время заполнения карточки 112 снова попадает в оценку.
2026-09-26 21:38:09 +00:00
GGlamer
c0fa0a50a1 Merge branch 'feat/finish-card-status'
fix: card_status в отчёте ДДС берётся у карточки, активной при завершении
2026-09-27 00:12:18 +03:00
gglamer
12329e7c5d fix: card_status в отчёте ДДС берётся у карточки, активной при завершении 2026-09-26 21:12:00 +00:00
GGlamer
783b63a683 Merge branch 'feat/session-report-hook'
refactor: общий hook отчёта сессии и объект параметров useControl.start
2026-09-27 00:09:40 +03:00
gglamer
3dc116534d refactor: useControl.start принимает объект параметров вместо 14 позиционных 2026-09-26 20:59:50 +00:00
gglamer
f6d0653e68 refactor: отчёт сессии загружается одним hook useSessionReport с повтором 409 2026-09-26 20:59:50 +00:00
gglamer
a36ec619e4 fix: пульт не теряет команду при опросе fencing — чтение сокета больше не отменяется 2026-09-26 20:58:02 +00:00
gglamer
93c5f063f3 fix: кириллический шрифт DejaVu в пакете для экспорта PDF без системных шрифтов 2026-09-26 20:47:10 +00:00
GGlamer
02c86e457d Merge branch 'feat/dds-ack-limit'
lct-11: срок решения на пульте ДДС из снимка сервера
2026-09-26 23:01:15 +03:00
GGlamer
69de83f232 Merge branch 'feat/checkpoint-fields'
fix: снимок сессии сохраняет раскрытые факты и поля завершённых карточек ДДС
2026-09-26 23:01:11 +03:00
gglamer
47bda8fba9 lct-11: срок решения на пульте ДДС из снимка сервера 2026-09-26 19:58:17 +00:00
gglamer
83b7f8ffdd fix: timer.tick доходит до пульта ДДС 2026-09-26 19:57:43 +00:00
gglamer
eb5de96ed7 fix: снимок сессии сохраняет раскрытые факты и поля завершённых карточек ДДС 2026-09-26 19:56:50 +00:00
gglamer
1d6b4d67da fix: отчёт и чек-лист при передаче 112→ДДС по сценарию оператора 2026-09-26 19:55:12 +00:00
gglamer
491655de5c fix: сокеты dev не закрываются при недоступной БД поколений 2026-09-26 19:27:52 +00:00
gglamer
fb9c27977d perf: разбор библиотеки сценариев один раз на прогон тестов 2026-09-26 19:27:52 +00:00
gglamer
d26cf2365d perf: разбор YAML сценариев через libyaml 2026-09-26 19:27:52 +00:00
gglamer
daa4f8c4f1 fix: подключить рекомендации модели к разбору, убрать лишнюю проверку грамматики ответа ДДС, задать настроение персоне worried 2026-09-26 17:14:04 +00:00
gglamer
3ae682a354 docs: актуализировать документацию под текущий продукт и код
Продукт сместился на упражнение ДДС с готовой карточкой, а часть
документов описывала голосовой тренажёр, LLM-судью и несуществующие
модули. Всё сверено с кодом.

- tasks/README.md: статусы карточек по коду, lct-13 снята
- arch: дерево модулей BACKEND/FRONTEND по коду, STT по умолчанию
  Whisper small, события и маршруты CONTRACT по events.py и API
- product: разбор, методика, режимы и звонящий без судьи и
  нереализованных функций (автопроигрывание эталона, ЭРА-ГЛОНАСС)
- spec, hackathon: нормативы настраиваемые, формат сценария по схеме,
  питч под ДДС, исторические решения помечены датой
- удалены посторонний 1.md и отработанные гайды lct-01 из корня
2026-09-26 17:13:56 +00:00
andreysk0304
7237265833 Complete training workflow and acceptance hardening 2026-09-26 17:13:45 +00:00
andreysk0304
4c4b91064f Complete DDS training workflow and delivery package 2026-09-24 01:10:49 +03:00
andreysk0304
68dd83c7c2 Complete incident selection and 112 to DDS workflow 2026-09-21 20:10:57 +03:00
andreysk0304
0526b11f91 Implement DDS exercise, customer UI and local demo 2026-09-21 19:56:13 +03:00
andreysk0304
cec84ffcd0 Implement local task 09 training workflows 2026-09-21 17:40:54 +03:00
Ivan Gerasimov
e081fa410b новое чуть 2026-09-20 10:05:43 +03:00
Ivan Gerasimov
9b74e3e524 feat: все 96 вызовов из билетов заказчика в библиотеке (lct-34)
Перенесены все 32 билета учебного центра, по три вызова в каждом. Это тот
материал, по которому заказчик учит операторов сегодня, — библиотека больше
не состоит из придуманных нами ситуаций.

Состав: медицина 23, пожары 19, полиция 16, ЖКХ 5, прочее 33. Исходы:
карточка 86, передача в другой регион 8, справка 2. С уточнением адреса —
29 вызовов. Уровни L1/L2/L3 — 11/38/47.

Написаны чек-листы, которых не было: medical, police, utility.

Признаки ЕКП подбирались под каждый вызов и проверены загрузчиком: комбинация,
которой нет в классификаторе, роняет загрузку. Несколько раз это и случилось,
и ошибка называла, чего не хватает.

Эталонных ответов в сценариях намеренно нет: в билетах их нет тоже, их знает
преподаватель, а наш эталон выводится кодом из фактов. Факты каждого вызова —
ровно то, что названо в строке билета.

193 теста зелёных, make seed заливает 97 сценариев.
2026-09-20 09:32:56 +03:00
Ivan Gerasimov
641e505c67 feat: АРМ администратора — записи, состояние стенда, аудит, копии (lct-24)
Целый раздел ТЗ, от которого не было ни одной функции.

- Учётные записи: завести, сменить роль и службу, заблокировать, сбросить
  пароль. Обучающемуся заводится и карточка курсанта — на ней висят профиль
  и проверка «это твой разбор».
- Состояние компонентов: база, модели речи, эмбеддинги, провайдер LLM, версия
  классификатора, число живых занятий. Отдельной строкой — секрет сессии:
  значение по умолчанию не ошибка запуска, но на стенде это дыра, и увидеть
  её должен администратор, а не проверяющий на защите.
- Журнал действий с фильтром, только на чтение.
- Резервные копии: кнопка и make backup для cron. pg_dump берётся локальный,
  а если его нет — из контейнера базы; если нет ни того ни другого,
  администратор видит, чего не хватает, а кнопка не молчит.

Границы роли выражены отсутствием маршрутов, а не проверками внутри них:
в разделе нет ни одной точки, трогающей оценки или пишущей в аудит. Два теста
проверяют это перебором маршрутов роутера — так запрет нельзя ослабить,
случайно добавив обработчик.

Найдено по ходу: тесты с базой падали в компании и проходили поодиночке.
Движок SQLAlchemy кэшировался на процесс, а каждый TestClient поднимает свой
событийный цикл — пул привязан к первому. Появился db.base.reset() и фикстура,
дающая движок на тест.

193 теста зелёных (12 новых).
2026-09-20 09:11:12 +03:00
Ivan Gerasimov
d55c8bc8cd 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
Ivan Gerasimov
4c2bbfd021 feat: билеты 1–3 заказчика в библиотеку сценариев (lct-34, частично)
Девять вызовов из билетов учебного центра в scenarios/tickets/. Библиотека
стала вложенной: загрузчик ходит по подкаталогам, checklists/ и pregenerated/
сценариями не считаются. Поля ticket и position задаются вместе.

Написаны чек-листы, которых не было: medical, police, utility. Без них
переносить было не на что — в библиотеке существовал только пожарный.

Девять вызовов закрывают все приёмы, ради которых билеты и брались: адрес
по ориентиру, адрес из другого региона, линейный объект (съезд с МКАД),
непрофильный вызов, профильный но не экстренный, массовая драка, где список
оповещения поднимает и полицию, и скорую.

Подводный камень записан в SCENARIO-FORMAT: значения признаков ЕКП бывают
с запятыми («Человек в воде, на льдине»), и в короткой записи YAML разрывает
такое значение на два.

Осталось семь билетов — работа механическая, рецепт отработан на трёх.

161 тест зелёный.
2026-09-20 08:38:01 +03:00
Ivan Gerasimov
b8dc39fd7d 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
Ivan Gerasimov
d4f0d0a0f8 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
Ivan Gerasimov
842a333d16 feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.

До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.

- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
  уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
  офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
  метрики address: «записал не тот адрес» и «не спросил» — разные навыки
  и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
  в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
  Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
  штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
  заказчика (билет 3, вызов 1).

Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.

125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
Ivan Gerasimov
7212251112 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