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

34 lines
1.5 KiB
Python
Raw Normal View History

lct-12: детерминированная оценка по ГОСТ, таксономия, радар Каждая метрика — факт против норматива со ссылкой: «94 с при ≤ 75 с, ГОСТ Р 22.7.03-2021», а не балл. По недобытому факту — отдельная отметка E1 с эталонным вопросом: в разборе нужен конкретный вопрос, не процент. Радар — проекция тех же метрик без пересчёта весов, коммуникация без судьи не рисуется нулём. Таймер, не остановленный событием, — провал, а не зачёт: время недоказуемо, операция не завершена. Метрика, которую нечем посчитать (полнота опроса без модели эмбеддингов), видна как «не посчитано» — молча выброшенная выглядела бы пройденной. Найдено противоречие: таймеры опроса (75 с) и оповещения ДДС (60 с) стартовали на ответе и останавливались передачей в ДДС — один отрезок, два лимита. Опрос за законные 70 с давал E3, а на экране курсанта краснел таймер посреди нормального разговора. События «опрос закончен» в контракте нет, поэтому dds_notify снят с учёта, вопрос записан в CONTRACT.md. KIO проверяет присваивание: card.dds = "03" клало в карточку сырую строку вместо кода ДДС, и падала уже оценка, далеко от места ошибки.
2026-09-17 14:06:16 +03:00
"""Радар шести компетенций.
Чистая проекция уже посчитанных метрик, без пересчёта весов: радар и оценка
всегда согласованы, и расхождение между ними невозможно по построению
(docs/product/METHODOLOGY.md#компетентная-модель).
"""
from app.domain.events import CompetencyScore, Metric
from app.domain.taxonomy import Competency
from app.scoring.taxonomy import METRIC_MAP
def radar(metrics: list[Metric]) -> list[CompetencyScore]:
"""Доля пройденного веса по каждой компетенции, 0–1.
Компетенция без метрик не рисуется нулём: «коммуникация» без судьи —
это «не оценивалось», а не «провалено».
"""
total: dict[Competency, float] = {}
passed: dict[Competency, float] = {}
for metric in metrics:
mapping = METRIC_MAP.get(metric.key)
if mapping is None:
continue
competency = mapping[1]
total[competency] = total.get(competency, 0.0) + metric.weight
if metric.passed:
passed[competency] = passed.get(competency, 0.0) + metric.weight
return [
CompetencyScore(competency=competency.value, value=round(passed.get(competency, 0.0) / weight, 3))
for competency in Competency
if (weight := total.get(competency))
]