refactor: одна функция метрики норматива времени для карточки 112 и карточки ДДС

This commit is contained in:
gglamer 2026-09-27 08:06:05 +00:00
commit a11206527a
2 changed files with 141 additions and 1 deletions

View file

@ -5,9 +5,94 @@
нулевое время даёт полный вклад, два норматива и более — нулевой. нулевое время даёт полный вклад, два норматива и более — нулевой.
""" """
from dataclasses import dataclass
from app.domain.events import Metric
from app.domain.taxonomy import Competency, ErrorCode, Finding, FindingSource
from app.scoring.taxonomy import METRIC_WEIGHTS
TIME_REF = "ТЗ задачи № 9; сессия вопросов и ответов 16.09.2026"
def time_credit(elapsed_ms: int | None, limit_ms: int) -> float: def time_credit(elapsed_ms: int | None, limit_ms: int) -> float:
"""Доля веса временной метрики, 0–1; отсутствие доказанного времени — 0.""" """Доля веса временной метрики, 0–1; отсутствие доказанного времени — 0."""
if elapsed_ms is None or elapsed_ms < 0 or limit_ms <= 0: if elapsed_ms is None or elapsed_ms < 0 or limit_ms <= 0:
return 0.0 return 0.0
return round(max(0.0, min(1.0, 1.0 - elapsed_ms / (2 * limit_ms))), 4) return round(max(0.0, min(1.0, 1.0 - elapsed_ms / (2 * limit_ms))), 4)
@dataclass(frozen=True)
class TimeNorm:
"""Формулировки норматива времени: карточка 112 и карточка ДДС различаются
только словами, расчёт и отметка E3 у них общие."""
key: str
title: str
summary: str
action: str
unmeasured: str
incomplete: str
CARD_FILL_TIME = TimeNorm(
key="card_fill_time",
title="Время заполнения карточки",
summary="Время заполнения карточки: норматив не выполнен",
action="сдать карточку",
unmeasured="время не зафиксировано",
incomplete="карточка не сдана",
)
DDS_WORK_TIME = TimeNorm(
key="dds_work_time",
title="Отработка карточки ДДС",
summary="ДДС: норматив времени отработки карточки не выполнен",
action="завершить",
unmeasured="время обработки не зафиксировано",
incomplete="карточка не завершена",
)
def time_metric(
norm: TimeNorm, elapsed_ms: int | None, limit_ms: int, *, completed: bool
) -> tuple[Metric, Finding | None]:
"""Метрика норматива и отметка E3, если он не выполнен.
Незавершённая работа не получает временного вклада: быстрое, но брошенное
заполнение не должно выглядеть скоростью.
"""
if elapsed_ms is None:
fact = norm.unmeasured
else:
delta_ms = elapsed_ms - limit_ms
if delta_ms > 0:
deviation = f"+{round(delta_ms / 1000)} с сверх норматива"
elif delta_ms < 0:
deviation = f"на {round(abs(delta_ms) / 1000)} с быстрее норматива"
else:
deviation = "точно в норматив"
fact = f"{round(elapsed_ms / 1000)} с ({deviation})"
if not completed:
fact = f"{norm.incomplete}; {fact}"
norm_text = f"{norm.action} за {round(limit_ms / 1000)} с"
passed = completed and elapsed_ms is not None and elapsed_ms <= limit_ms
metric = Metric(
key=norm.key,
title=norm.title,
fact=fact,
norm=norm_text,
ref=TIME_REF,
passed=passed,
weight=METRIC_WEIGHTS[norm.key],
credit=time_credit(elapsed_ms, limit_ms) if completed else 0.0,
)
if passed:
return metric, None
return metric, Finding(
code=ErrorCode.E3,
source=FindingSource.TIMERS,
summary=norm.summary,
fact=fact,
norm=norm_text,
ref=TIME_REF,
competency=Competency.NORMS,
)

View file

@ -1,9 +1,10 @@
import pytest import pytest
from app.domain.events import Metric from app.domain.events import Metric
from app.domain.taxonomy import Competency, ErrorCode, FindingSource
from app.scoring.competency import radar from app.scoring.competency import radar
from app.scoring.gost import GostResult from app.scoring.gost import GostResult
from app.scoring.timing import time_credit from app.scoring.timing import CARD_FILL_TIME, DDS_WORK_TIME, time_credit, time_metric
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -23,3 +24,57 @@ def test_fractional_timing_credit_affects_total_and_competency_scores():
result = GostResult(metrics=[metric]) result = GostResult(metrics=[metric])
assert result.score == 75.0 assert result.score == 75.0
assert radar([metric])[0].value == 0.75 assert radar([metric])[0].value == 0.75
@pytest.mark.parametrize(
("elapsed_ms", "completed", "fact", "passed"),
[
(200_000, True, "200 с (+20 с сверх норматива)", False),
(61_000, True, "61 с (на 119 с быстрее норматива)", True),
(180_000, True, "180 с (точно в норматив)", True),
(None, True, "время не зафиксировано", False),
(30_000, False, "карточка не сдана; 30 с (на 150 с быстрее норматива)", False),
],
)
def test_card_fill_time_metric_texts(elapsed_ms, completed, fact, passed):
metric, finding = time_metric(CARD_FILL_TIME, elapsed_ms, 180_000, completed=completed)
assert metric.key == "card_fill_time"
assert metric.title == "Время заполнения карточки"
assert metric.fact == fact
assert metric.norm == "сдать карточку за 180 с"
assert metric.ref == "ТЗ задачи № 9; сессия вопросов и ответов 16.09.2026"
assert metric.passed is passed
assert metric.credit == (time_credit(elapsed_ms, 180_000) if completed else 0.0)
if passed:
assert finding is None
else:
assert finding.code is ErrorCode.E3
assert finding.source is FindingSource.TIMERS
assert finding.competency is Competency.NORMS
assert finding.summary == "Время заполнения карточки: норматив не выполнен"
assert (finding.fact, finding.norm) == (fact, "сдать карточку за 180 с")
@pytest.mark.parametrize(
("elapsed_ms", "completed", "fact", "passed"),
[
(605_000, True, "605 с (+425 с сверх норматива)", False),
(25_000, True, "25 с (на 155 с быстрее норматива)", True),
(180_000, True, "180 с (точно в норматив)", True),
(None, False, "карточка не завершена; время обработки не зафиксировано", False),
(25_000, False, "карточка не завершена; 25 с (на 155 с быстрее норматива)", False),
],
)
def test_dds_work_time_metric_texts(elapsed_ms, completed, fact, passed):
metric, finding = time_metric(DDS_WORK_TIME, elapsed_ms, 180_000, completed=completed)
assert metric.key == "dds_work_time"
assert metric.title == "Отработка карточки ДДС"
assert metric.fact == fact
assert metric.norm == "завершить за 180 с"
assert metric.passed is passed
assert (finding is None) is passed
if finding is not None:
assert finding.summary == "ДДС: норматив времени отработки карточки не выполнен"
assert (finding.fact, finding.norm) == (fact, "завершить за 180 с")