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 чистый.
This commit is contained in:
Ivan Gerasimov 2026-09-20 08:33:30 +03:00
commit b8dc39fd7d
22 changed files with 628 additions and 9 deletions

View file

@ -0,0 +1,53 @@
# Билет 1, вызов 1 (docs/spec/TICKETS.md). Ситуация и адрес — дословно из билета,
# факты и чек-лист выведены из ситуации: чего в строке нет, того звонящий не знает.
id: t01-1-fire-container
title: "Возгорание мусорного контейнера"
ticket: 1
position: 1
type: fire
level: L2
topics: [fires]
modes: [training, exam, self]
extends: checklists/fire.yaml
signs: [на улице, мусор, открытое пламя]
persona:
base: calm
arc:
- { stage: registration, mood: calm }
- { stage: interview, mood: calm }
first_line: "Здравствуйте. Тут мусорный контейнер горит, у депо."
facts:
- id: f_address
value: "Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции"
refined: "Московская железная дорога, Киевская, 1 км, дом 2, строение 2"
reveal_on: { question: q_address }
refine_on: q_address_check
- id: f_what_burns
value: "горит мусорный контейнер, пламя открытое"
reveal_on: { question: q_what_burns }
- id: f_people
value: "пострадавших нет, рядом никого"
reveal_on: { question: q_people }
- id: f_caller
value: "Сидоров Иван Сергеевич, телефон 916-126-34-71"
reveal_on: { question: q_caller }
checklist:
- { id: q_address, fact: f_address }
- { id: q_address_check }
- { id: q_what_burns, fact: f_what_burns }
- { id: q_people, fact: f_people }
- { id: q_caller, fact: f_caller }
required_fields: [address, incident_type, description]
ground_truth:
address: "Московская железная дорога, Киевская, 1 км, дом 2, строение 2"
victims: 0
tree:
pregenerated: false

View file

@ -0,0 +1,58 @@
# Билет 1, вызов 2 (docs/spec/TICKETS.md). Массовая драка с пострадавшими:
# в списке оповещения и полиция, и скорая — это тот случай, где выбор одной
# службы был бы неверен по построению.
id: t01-2-mass-fight
title: "Массовая драка у посольства, пятеро пострадавших"
ticket: 1
position: 2
type: police
level: L2
topics: [police]
modes: [training, exam, self]
extends: checklists/police.yaml
signs: [Драка, Улица общественное место, Массовая (от 10 человек)]
persona:
base: worried
arc:
- { stage: registration, mood: worried }
- { stage: interview, mood: worried }
first_line: "Алло! Тут человек пятнадцать дерутся, прутами машут! Скорее!"
facts:
- id: f_address
value: "рядом с посольством Азербайджана, на тротуаре"
refined: "Леонтьевский переулок, дом 16, строение 1"
reveal_on: { question: q_address }
refine_on: q_address_check
- id: f_what_happened
value: "дерутся десять-пятнадцать человек"
reveal_on: { question: q_what_happened }
- id: f_victims
value: "пятеро пострадавших с разными травмами"
reveal_on: { question: q_victims }
- id: f_suspects
value: "дерутся палками и железными прутами, огнестрельного не видно"
reveal_on: { question: q_suspects }
- id: f_caller
value: "Иванов Пётр Иванович, телефон 916-123-98-78"
reveal_on: { question: q_caller }
checklist:
- { id: q_address, fact: f_address }
- { id: q_address_check }
- { id: q_what_happened, fact: f_what_happened }
- { id: q_victims, fact: f_victims }
- { id: q_suspects, fact: f_suspects }
- { id: q_caller, fact: f_caller }
required_fields: [address, incident_type, description, victims_count]
ground_truth:
address: "Леонтьевский переулок, дом 16, строение 1"
victims: 5
tree:
pregenerated: false

View file

@ -0,0 +1,64 @@
# Билет 1, вызов 3 (docs/spec/TICKETS.md). Вызов не из Москвы: Волжский —
# это Волгоградская область. Правильное действие — не заводить карточку,
# а передать вызов в систему-112 другого субъекта. Проверяется тем, что
# курсант вообще спросил про город.
id: t01-3-child-other-region
title: "Ребёнок упал с велосипеда, вызов из другого региона"
ticket: 1
position: 3
type: medical
level: L3
topics: [medical, address]
modes: [training, exam, self]
extends: checklists/medical.yaml
signs: [Травма]
outcome: transfer_region
persona:
base: worried
arc:
- { stage: registration, mood: worried }
- { stage: interview, mood: worried }
first_line: "Здравствуйте, сын с велосипеда упал, рука и нога опухли!"
facts:
# Мама называет улицу так, будто это Москва. Про область она скажет только
# если спросить: ей не приходит в голову, что попала не туда.
- id: f_address
value: "улица Карла Маркса, около Молсыркомбината"
refined: "Волгоградская область, город Волжский, улица Карла Маркса, около Волжского Молсыркомбината"
reveal_on: { question: q_address }
refine_on: q_address_check
- id: f_what_happened
value: "упал с велосипеда сам, отёк руки и ноги"
reveal_on: { question: q_what_happened }
- id: f_who
value: "Смирнов Илья, 11 лет, сын"
reveal_on: { question: q_who }
- id: f_conscious
value: "в сознании, плачет, дышит нормально"
reveal_on: { question: q_conscious }
- id: f_caller
value: "звонит мама, телефон 9163201283"
reveal_on: { question: q_caller }
checklist:
- { id: q_address, fact: f_address }
- { id: q_address_check }
- { id: q_what_happened, fact: f_what_happened }
- { id: q_who, fact: f_who }
- { id: q_conscious, fact: f_conscious }
- { id: q_caller, fact: f_caller }
# Карточка здесь не заводится, поэтому обязательных полей нет: штрафовать
# за пустую карточку значит учить заполнять лишнее.
required_fields: []
ground_truth:
address: "Волгоградская область, город Волжский, улица Карла Маркса, около Волжского Молсыркомбината"
victims: 1
tree:
pregenerated: false