feat: реагирование смежных служб в карточке ДДС и вводная «бригада просит смежников»
This commit is contained in:
parent
b7445a60fe
commit
de624d09f0
19 changed files with 1139 additions and 21 deletions
138
backend/app/scoring/adjacent.py
Normal file
138
backend/app/scoring/adjacent.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Оценка работы курсанта со смежными службами: вводная «бригада просит смежников».
|
||||
|
||||
Ответ заказчика (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
|
||||
|
|
@ -32,6 +32,8 @@ METRIC_MAP: dict[str, tuple[ErrorCode, Competency]] = {
|
|||
"dds_completion": (ErrorCode.D6, Competency.CARD),
|
||||
"dds_reply": (ErrorCode.D5, Competency.COMMUNICATION),
|
||||
"dds_report_reaction": (ErrorCode.D6, Competency.NORMS),
|
||||
"dds_adjacent_check": (ErrorCode.D6, Competency.CARD),
|
||||
"dds_adjacent_call": (ErrorCode.D6, Competency.COMMUNICATION),
|
||||
"dds_grammar": (ErrorCode.D5, Competency.COMMUNICATION),
|
||||
"description_grammar": (ErrorCode.E4, Competency.COMMUNICATION),
|
||||
}
|
||||
|
|
@ -66,4 +68,6 @@ METRIC_WEIGHTS: dict[str, float] = {
|
|||
"card_fill_time": 1.5,
|
||||
"dds_work_time": 1.5,
|
||||
"dds_report_reaction": 1.5,
|
||||
"dds_adjacent_check": 1.5,
|
||||
"dds_adjacent_call": 1.5,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue