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

138 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Оценка работы курсанта со смежными службами: вводная «бригада просит смежников».
Ответ заказчика (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