"""브라우저용 세션 projection이 공유하는 순수 도우미.""" from __future__ import annotations from datetime import datetime, timezone from typing import cast from .services import state_machine from .stage_contract import StageLabel, stage_label as _normalize_stage_label from .store import InProcSession, TurnRecord LEARNER_VISIBLE_AI_ROLE = "counselor" # 전 주기(라포→정리) 기준 라포 만점: 마지막 전이 임계(개입→정리)를 100으로 본다. _FULL_CYCLE_RAPPORT = max(state_machine.STAGE_ADVANCE_RAPPORT.values()) def stage_label(stage: object) -> StageLabel: return cast(StageLabel, _normalize_stage_label(stage)) def iso(ts: float | None) -> str | None: if ts is None: return None return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds") def learner_visible_turns(sess: InProcSession) -> list[TurnRecord]: return sess.turns_visible_to(LEARNER_VISIBLE_AI_ROLE) def _rapport_percent(credit: float) -> int: if _FULL_CYCLE_RAPPORT <= 0: return 0 return max(0, min(100, int(round(float(credit or 0.0) / _FULL_CYCLE_RAPPORT * 100))))