lct-hack/backend/app/scoring/adjacent.py

138 lines
8.6 KiB
Python
Raw Normal View History

"""Оценка работы курсанта со смежными службами: вводная «бригада просит смежников».
Ответ заказчика (28.09): статусы и комментарии в карточке в первую очередь
читают другие ДДС, участвующие в ликвидации. Проверяются два действия —
сверился ли курсант с карточкой перед ответом бригаде и связался ли со
смежной службой, когда та молчит. Обе метрики детерминированные, по событиям
занятия, и обе размечаются кодом D6: неполный ход работ в карточке скрывает
от смежных служб факт реакции (классификатор D1–D6 не расширяется).
"""
from collections.abc import Sequence
from datetime import datetime
from app.domain.events import Metric
from app.domain.statuses import ADJACENT_REACTING, StatusEntry
from app.domain.taxonomy import Competency, ErrorCode, Finding, FindingSource
from app.scoring.taxonomy import METRIC_WEIGHTS
ADJACENT_REF = "памятка «Работа на АРМ-112», статусы реагирования; ответ заказчика 28.09"
def active_ms(start: datetime, end: datetime,
pauses: Sequence[tuple[datetime, datetime]]) -> int:
"""Активное время между моментами: закрытые интервалы пауз вычитаются."""
elapsed = int((end - start).total_seconds() * 1000)
for pause_start, pause_end in pauses:
overlap = (min(end, pause_end) - max(start, pause_start)).total_seconds()
if overlap > 0:
elapsed -= int(overlap * 1000)
return max(0, elapsed)
def _reacted_before(entries: list[StatusEntry], service: str, moment: datetime) -> bool:
"""Реакция смежной службы в карточке к моменту: любой статус реагирования.
Автор не важен: отметка после звонка курсанта — такая же реакция, как
сценарная, и успокоить бригаду после неё правомерно.
"""
return any(entry.service == service and entry.status in ADJACENT_REACTING
and entry.at <= moment for entry in entries)
def _metric(key: str, title: str, passed: bool, fact: str, norm: str, service: str) -> Metric:
return Metric(key=key, title=f"{service}: {title}", fact=fact, norm=norm,
ref=ADJACENT_REF, passed=passed, service=service,
weight=METRIC_WEIGHTS.get(key, 1.0))
def _finding(service: str, competency: Competency, summary: str, fact: str,
norm: str) -> Finding:
return Finding(service=service, code=ErrorCode.D6, source=FindingSource.DISPATCHER,
summary=f"{service}: {summary}", fact=fact, norm=norm,
ref=ADJACENT_REF, competency=competency)
def adjacent_metrics(
card, limit_ms: int,
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
) -> tuple[list[Metric], list[Finding]]:
"""Метрики и отметки по вводной. Пусто — вводной не было или она не пришла.
Исход «реагирует / молчит» — следствие таймлайна сценария, отдельной
ручки нет. Перестраховочный звонок смежной службе при её реагировании
ошибкой не считается: метрика звонка в этом исходе не выставляется.
"""
request = card.scenario.crew_request
if request is None or card.crew_request_at is None:
return [], []
pauses = list(pause_intervals or [])
service = request.service
reacting_variant = any(
entry.service == service and entry.status in ADJACENT_REACTING
for entry in card.scenario.adjacent_timeline
)
reply_delay_ms = (active_ms(card.crew_request_at, card.adjacent_reply_at, pauses)
if card.adjacent_reply_at is not None else None)
call_delay_ms = (active_ms(card.crew_request_at, card.adjacent_call_at, pauses)
if card.adjacent_call_at is not None else None)
limit_s = limit_ms // 1000
metrics: list[Metric] = []
findings: list[Finding] = []
check_norm = ("свериться с журналом карточки и передать бригаде фактическое "
"состояние реагирования смежной службы")
if card.adjacent_reply_claimed is True:
reacted = _reacted_before(card.status_log, service, card.adjacent_reply_at)
late = reacting_variant and reply_delay_ms is not None and reply_delay_ms > limit_ms
if not reacted:
fact = ("бригаде передано «смежная служба реагирует», "
"хотя отметок её реакции в карточке не было")
elif late:
fact = (f"ответ бригаде через {reply_delay_ms // 1000} с "
f"при лимите {limit_s} с")
else:
fact = f"ответ бригаде сверен с карточкой: {service} реагирует"
metrics.append(_metric("dds_adjacent_check", "сверка с карточкой перед ответом бригаде",
reacted and not late, fact, check_norm, service))
if not reacted:
findings.append(_finding(service, Competency.CARD,
"бригада успокоена без сверки с карточкой",
fact, check_norm))
elif late:
findings.append(_finding(service, Competency.CARD,
"ответ бригаде позже лимита вводной",
fact, check_norm))
elif card.adjacent_reply_claimed is False:
if reacting_variant:
metrics.append(_metric(
"dds_adjacent_check", "сверка с карточкой перед ответом бригаде", True,
"бригаде передано, что сведения будут уточнены", check_norm, service))
elif reacting_variant:
fact = "входящий вызов бригады остался без ответа до закрытия карточки"
metrics.append(_metric("dds_adjacent_check",
"сверка с карточкой перед ответом бригаде",
False, fact, check_norm, service))
findings.append(_finding(service, Competency.CARD,
"запрос бригады о смежной службе проигнорирован",
fact, check_norm))
if not reacting_variant:
call_norm = (f"при молчании смежной службы связаться с ней и ответить "
f"бригаде не позже {limit_s} с от вводной")
reply_ok = reply_delay_ms is not None and reply_delay_ms <= limit_ms
call_ok = call_delay_ms is not None and call_delay_ms <= limit_ms
call_fact = (f"звонок «{service}» через {call_delay_ms // 1000} с"
if call_delay_ms is not None else f"звонка «{service}» не было")
reply_fact = (f"ответ бригаде через {reply_delay_ms // 1000} с"
if reply_delay_ms is not None else "ответа бригаде не было")
fact = f"{call_fact}; {reply_fact}"
metrics.append(_metric("dds_adjacent_call", "связь со смежной службой при её молчании",
reply_ok and call_ok, fact, call_norm, service))
if not (reply_ok and call_ok):
summary = ("молчание смежной службы осталось без звонка"
if call_delay_ms is None
else "запрос бригады о смежной службе не закрыт в лимит")
findings.append(_finding(service, Competency.COMMUNICATION, summary,
fact, call_norm))
return metrics, findings