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