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:
parent
d4f0d0a0f8
commit
b8dc39fd7d
22 changed files with 628 additions and 9 deletions
|
|
@ -65,7 +65,12 @@ def _derive_ground_truth(scenario: Scenario) -> Scenario:
|
|||
f"версии {ekp.reference().version}"
|
||||
)
|
||||
scenario.ground_truth.incident_code = found.code
|
||||
scenario.ground_truth.notify = ekp.notify_list(found.code)
|
||||
# Эталонный список оповещения — с учётом обстоятельств вызова: пятеро
|
||||
# пострадавших поднимают скорую, и без них эталон был бы неполон.
|
||||
keys: list[str] = []
|
||||
if scenario.ground_truth.victims:
|
||||
keys.append("casualties")
|
||||
scenario.ground_truth.notify = ekp.notify_list(found.code, keys)
|
||||
return scenario
|
||||
|
||||
|
||||
|
|
@ -154,14 +159,32 @@ def load_file(path: Path, root: Path) -> Scenario:
|
|||
return _derive_ground_truth(scenario)
|
||||
|
||||
|
||||
#: Подкаталоги, которые сценариями не являются.
|
||||
NOT_SCENARIOS = {"checklists", "pregenerated"}
|
||||
|
||||
|
||||
def scenario_files(root: Path) -> list[Path]:
|
||||
"""Файлы сценариев, включая подкаталоги вроде `tickets/`.
|
||||
|
||||
Библиотека перестала быть плоской, когда в неё поехали билеты заказчика:
|
||||
тридцать два билета по три вызова — это девяносто шесть файлов, и держать
|
||||
их в одном каталоге рядом с чек-листами нельзя (docs/spec/TICKETS.md).
|
||||
"""
|
||||
return sorted(
|
||||
path
|
||||
for path in root.rglob("*.yaml")
|
||||
if not set(path.relative_to(root).parts[:-1]) & NOT_SCENARIOS
|
||||
)
|
||||
|
||||
|
||||
def load_library(root: Path) -> list[Scenario]:
|
||||
"""Все сценарии каталога. Подкаталог `checklists/` — не сценарии."""
|
||||
"""Все сценарии каталога и его подкаталогов."""
|
||||
if not root.exists():
|
||||
raise ScenarioError(f"каталог сценариев не найден: {root}")
|
||||
|
||||
scenarios: list[Scenario] = []
|
||||
seen: dict[str, Path] = {}
|
||||
for path in sorted(root.glob("*.yaml")):
|
||||
for path in scenario_files(root):
|
||||
scenario = load_file(path, root)
|
||||
if scenario.id in seen:
|
||||
raise ScenarioError(f"{path.name}: id {scenario.id} уже занят {seen[scenario.id].name}")
|
||||
|
|
|
|||
Loading…
Reference in a new issue