전 저장소 리팩터링과 SSOT 정비

This commit is contained in:
Yun Chan 2026-07-15 21:31:30 +09:00
parent 14ecbd4e7d
commit 3dfddcac6f
173 changed files with 19679 additions and 6952 deletions

View file

@ -16,13 +16,13 @@ from typing import Any, Literal, Optional, cast
from pydantic import BaseModel, Field
from .config import settings
from .services import guardrail, session_metrics
from .services import guardrail, session_metrics, state_machine
from .stage_contract import (
ReviewPhaseKey,
StageLabel,
review_phase_key,
stage_label as _normalize_stage_label,
stage_label_or_none,
stage_label_or_none as stage_label_or_none,
)
from .store import InProcSession, TurnRecord
@ -106,6 +106,8 @@ class LearnerDashboardPersonaProgress(BaseModel):
latest_stage: StageLabel | None = None
latest_score: float | None = None
trend: str = "insufficient"
# P2 누적 게이지: 이 페르소나와 쌓아온 라포 누적(전 주기 임계 기준 %).
rapport_percent: int = 0
class LearnerDashboardAchievement(BaseModel):
@ -147,6 +149,75 @@ class SessionArchiveResponse(BaseModel):
session: LearnerSessionSummary
class SessionStageProgress(BaseModel):
"""단계별 누적 게이지(P2). 상태머신 결정론 수치의 파생값만 담는다."""
stage: StageLabel
percent: int = 0
achieved: bool = False
is_goal: bool = False
class SessionProgress(BaseModel):
"""회기 진행 상세(P2). 내부 원값 명칭 대신 학습자-안전 파생 %만 노출한다."""
stages: list[SessionStageProgress] = Field(default_factory=list)
rapport_percent: int = 0
rapport_delta_percent: int = 0
resistance_percent: int = 0
openness_percent: int = 0
# 전 주기(라포→정리) 기준 라포 만점: 마지막 전이 임계(개입→정리)를 100으로 본다.
_FULL_CYCLE_RAPPORT = max(state_machine.STAGE_ADVANCE_RAPPORT.values())
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))))
def build_session_progress(
state: state_machine.SessionState,
*,
prev_rapport_credit: float = 0.0,
goal_stages: list[str] | None = None,
) -> SessionProgress:
"""상태머신 수치 → 단계 누적 게이지/상세 수치 파생(순수함수, LLM 미경유).
누적성: rapport_credit 회기 ×0.7 이월되므로 다음 회기 게이지는 이월분에서 시작한다.
"""
goals = {stage_label(goal) for goal in (goal_stages or [])}
order = state_machine.STAGE_ORDER
cur_idx = order.index(state.stage)
stages: list[SessionStageProgress] = []
for i, st in enumerate(order):
label = stage_label(st)
if i < cur_idx:
pct, achieved = 100, True
elif i > cur_idx:
pct, achieved = 0, False
elif st is state_machine.Stage.CLOSE:
pct, achieved = 100, True
else:
need = state_machine.STAGE_ADVANCE_RAPPORT.get(st, 1.0)
ratio = min(1.0, float(state.rapport_credit or 0.0) / need) if need > 0 else 0.0
pct, achieved = min(99, int(round(ratio * 99))), False
stages.append(
SessionStageProgress(stage=label, percent=pct, achieved=achieved, is_goal=label in goals)
)
rapport_pct = _rapport_percent(state.rapport_credit)
prev_pct = _rapport_percent(prev_rapport_credit)
return SessionProgress(
stages=stages,
rapport_percent=rapport_pct,
rapport_delta_percent=max(0, rapport_pct - prev_pct),
resistance_percent=max(0, min(100, int(round(float(state.resistance or 0.0) * 100)))),
openness_percent=max(0, min(100, int(round(float(state.effective_openness or 0.0) * 100)))),
)
class SessionDetailTurn(BaseModel):
turn_seq: int
speaker: Literal["learner", "client"]
@ -168,6 +239,12 @@ class SessionDetailResponse(BaseModel):
ended_at: str | None = None
turns: list[SessionDetailTurn] = Field(default_factory=list)
review_ready: bool = False
# 시간 기반 회기(2026-07-13 회의 P1): 새로고침 복원 시 타이머·목표 표시의 기준.
goal_stages: list[StageLabel] = Field(default_factory=list)
duration_limit_seconds: int = 0
warning_before_end_seconds: int = 0
# P2 단계 누적 게이지·상세 수치.
progress: SessionProgress | None = None
class ReviewClient(BaseModel):
@ -537,6 +614,7 @@ def dashboard_persona_progress(
latest_stage=stage_label(latest.state.stage),
latest_score=growth.latest_score if growth else None,
trend=growth.trend if growth else "insufficient",
rapport_percent=_rapport_percent(latest.state.rapport_credit),
)
)
return sorted(
@ -643,6 +721,14 @@ def session_detail(
for turn in turns
],
review_ready=review_ready,
goal_stages=[stage_label(goal) for goal in (sess.goal_stages or [])],
duration_limit_seconds=max(0, settings.session_duration_minutes) * 60,
warning_before_end_seconds=max(0, settings.session_warning_minutes) * 60,
progress=build_session_progress(
sess.state,
prev_rapport_credit=sess.prev_rapport_credit,
goal_stages=list(sess.goal_stages or []),
),
)