회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s

This commit is contained in:
Yun Chan 2026-09-08 23:28:06 +09:00
parent a479db7a5a
commit a0311c5957
100 changed files with 4884 additions and 11210 deletions

View file

@ -2,7 +2,6 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
@ -26,6 +25,13 @@ from .calibration_transfer import (
assess_synthetic_subgroup_drift,
assess_transfer,
)
from .outcome_repository_values import (
canonical_hash as _canonical_hash,
created_role as _created_role,
public_row as _public_row,
value as _value,
)
from .practice_competency import target_techniques
class CalibrationTransferNotFoundError(LookupError):
@ -64,36 +70,6 @@ _POSITIVE_CLIENT_STATES = frozenset(
)
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
except (KeyError, TypeError):
return default
def _public_row(row: Mapping[str, Any]) -> dict[str, Any]:
"""API 응답에서 학습자 피드백 정책 판정 전용 열을 제거한다."""
payload = dict(row)
payload.pop("source_learner_feedback_enabled", None)
return payload
def _canonical_hash(payload: Mapping[str, Any]) -> str:
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=str,
)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def _created_role(principal: Principal) -> str:
return "instructor" if principal.role == Role.TEACHER else principal.role.value
def _ensure_unique_evidence(
evidence_turn_ids: Sequence[UUID], *, required: bool = False
) -> tuple[UUID, ...]:
@ -182,6 +158,16 @@ async def append_prediction_revision(
reason = revision_reason.strip()
if not reason:
raise CalibrationTransferStateError("revision_reason must not be blank")
if not (0.0 <= predicted_success_probability <= 1.0):
raise CalibrationTransferStateError(
"predicted_success_probability must be between 0.0 and 1.0"
)
if not (0.0 <= confidence <= 1.0):
raise CalibrationTransferStateError(
"confidence must be between 0.0 and 1.0"
)
if instrument_id == "vignette.calibration-self-prediction":
instrument_id = "calibration-mirror-g5"
payload = {
"prediction_revision_id": str(prediction_revision_id),
"history_id": str(history_id),
@ -318,6 +304,11 @@ async def append_prediction_revision(
asyncpg.ForeignKeyViolationError,
asyncpg.ObjectNotInPrerequisiteStateError,
) as exc:
message = str(exc)
if "lock" in message.lower() or "reveal" in message.lower():
raise CalibrationTransferStateError(
f"self-prediction history is locked or revealed: {message}"
) from exc
raise CalibrationTransferStateError(
"prediction revision violated provenance or history invariants"
) from exc
@ -880,21 +871,9 @@ async def append_transfer_suite(
def _actual_target_techniques(competency_id: str) -> frozenset[str]:
key = competency_id.lower()
if any(token in key for token in ("empathy", "empathic", "reflection")):
return frozenset({"empathy", "reflection", "validation", "restatement"})
if any(token in key for token in ("open_question", "open-question")):
return frozenset({"facilitative_question", "exploration", "clarification"})
if any(token in key for token in ("rupture", "repair", "impact")):
return frozenset(
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
)
if any(token in key for token in ("goal", "collaborative", "reagreement")):
return frozenset(
{"consent_motivation_check", "opinion_check", "restatement"}
)
if any(token in key for token in ("presence", "response-space")):
return frozenset({"holding", "reflection", "here_and_now_focus"})
targets = target_techniques(competency_id)
if targets is not None:
return targets
raise CalibrationTransferStateError(
f"unsupported actual transfer competency: {competency_id}"
)

View file

@ -2,8 +2,6 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from typing import Any, Literal, Protocol
from uuid import UUID, uuid5
@ -26,6 +24,10 @@ from .continuous_improvement import (
promote_content_to_catalog,
release_allowed,
)
from .outcome_repository_values import (
canonical_hash as _canonical_hash,
value as _value,
)
DATA_CLASSIFICATION = "synthetic_replay_red_team_coverage_drift"
@ -101,24 +103,6 @@ class RollbackExecutor(Protocol):
) -> RollbackExecutionReceipt: ...
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
except (KeyError, TypeError):
return default
def _canonical_hash(payload: Any) -> str:
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=str,
)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
async def _begin_submission(
conn: asyncpg.Connection,
*,

View file

@ -7,8 +7,6 @@ separate superseding events and never mutate the original evidence or graph.
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from typing import Any
from uuid import UUID, uuid5
@ -39,6 +37,12 @@ from .practice_runtime_observer import (
derive_runtime_episode,
observation_model_run_id,
)
from .outcome_repository_values import (
canonical_hash as _canonical_hash,
created_role as _created_role,
public_row as _public_row,
value as _value,
)
_RUNTIME_ATTEMPT_NAMESPACE = UUID("52e24f06-34be-54cb-9092-5122e384c814")
@ -60,36 +64,6 @@ class DeliberatePracticeFeedbackDisabledError(PermissionError):
"""처방/연습 원천 회기의 학습자 피드백 스냅샷이 비활성이다."""
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
except (KeyError, TypeError):
return default
def _public_row(row: Mapping[str, Any]) -> dict[str, Any]:
"""API 응답에서 정책 판정 전용 내부 열을 제거한다."""
payload = dict(row)
payload.pop("source_learner_feedback_enabled", None)
return payload
def _canonical_hash(payload: Mapping[str, Any]) -> str:
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=str,
)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def _created_role(principal: Principal) -> str:
return "instructor" if principal.role == Role.TEACHER else principal.role.value
def _ensure_unique_evidence(
evidence_turn_ids: Sequence[UUID], *, required: bool = True
) -> tuple[UUID, ...]:

View file

@ -0,0 +1,40 @@
"""결과 저장소가 공유하는 순수 값 정규화 도우미."""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping
from typing import Any
from ..deps import Principal, Role
def value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
except (KeyError, TypeError):
return default
def canonical_hash(payload: Any) -> str:
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=str,
)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
def public_row(row: Mapping[str, Any]) -> dict[str, Any]:
"""API 응답에서 학습자 피드백 정책 판정 전용 열을 제거한다."""
payload = dict(row)
payload.pop("source_learner_feedback_enabled", None)
return payload
def created_role(principal: Principal) -> str:
return "instructor" if principal.role == Role.TEACHER else principal.role.value

View file

@ -0,0 +1,22 @@
"""실제 연습과 전이 평가가 공유하는 competency 분류."""
from __future__ import annotations
def target_techniques(competency_id: str) -> frozenset[str] | None:
key = competency_id.lower()
if any(token in key for token in ("empathy", "empathic", "reflection")):
return frozenset({"empathy", "reflection", "validation", "restatement"})
if any(token in key for token in ("open_question", "open-question")):
return frozenset({"facilitative_question", "exploration", "clarification"})
if any(token in key for token in ("rupture", "repair", "impact")):
return frozenset(
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
)
if any(token in key for token in ("goal", "collaborative", "reagreement")):
return frozenset(
{"consent_motivation_check", "opinion_check", "restatement"}
)
if any(token in key for token in ("presence", "response-space")):
return frozenset({"holding", "reflection", "here_and_now_focus"})
return None

View file

@ -19,6 +19,7 @@ from ..contracts.deliberate_practice import (
PracticePrescription,
ScenarioNovelty,
)
from .practice_competency import target_techniques
_OBSERVER_NAMESPACE = UUID("2d2df938-056c-56cc-86d2-99ad53bd3507")
@ -67,21 +68,9 @@ def observation_model_run_id(
def _target_techniques(competency_id: str) -> frozenset[str]:
key = competency_id.lower()
if any(token in key for token in ("empathy", "empathic", "reflection")):
return frozenset({"empathy", "reflection", "validation", "restatement"})
if any(token in key for token in ("open_question", "open-question")):
return frozenset({"facilitative_question", "exploration", "clarification"})
if any(token in key for token in ("rupture", "repair", "impact")):
return frozenset(
{"opinion_check", "validation", "reflection", "here_and_now_focus"}
)
if any(token in key for token in ("goal", "collaborative", "reagreement")):
return frozenset(
{"consent_motivation_check", "opinion_check", "restatement"}
)
if any(token in key for token in ("presence", "response-space")):
return frozenset({"holding", "reflection", "here_and_now_focus"})
targets = target_techniques(competency_id)
if targets is not None:
return targets
raise RuntimePracticeObservationError(
f"unsupported runtime practice competency: {competency_id}"
)

View file

@ -213,7 +213,12 @@ def build_learner_growth(
learner_id=learner_id,
learner_label=learner_label(learner_id),
sessions=len(ordered),
ended_sessions=sum(1 for sess in ordered if sess.ended),
ended_sessions=sum(
1
for sess in ordered
if sess.ended
and any(t.speaker in ("counselor", "learner") for t in sess.turns)
),
latest_at=iso_datetime(session_activity_time(latest_session)) or "",
first_score=first_score,
latest_score=latest_score,

View file

@ -2,8 +2,6 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from typing import Any
from uuid import UUID, uuid5
@ -23,6 +21,10 @@ from .supervision_research import (
build_phase3_outcome_manifest,
compare_evaluation_versions,
)
from .outcome_repository_values import (
canonical_hash as _canonical_hash,
value as _value,
)
_POINTER_NAMESPACE = UUID("f99f95ba-365e-46ea-a613-2239275f8a2d")
@ -40,24 +42,6 @@ class SupervisionResearchNotFoundError(SupervisionResearchError):
"""Required source evidence or a visible aggregate does not exist."""
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
except (KeyError, TypeError):
return default
def _canonical_hash(payload: Any) -> str:
serialized = json.dumps(
payload,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
default=str,
)
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
async def _existing_submission(
conn: asyncpg.Connection,
*,

View file

@ -193,6 +193,29 @@ class StreamingTranscriptEvent:
confidence: float | None = None
def _streaming_provider_event(
provider: str,
transcript_event: StreamingTranscriptEvent,
*,
start: float,
duration: float,
) -> dict[str, object]:
event_type = "speech_final" if transcript_event.speech_final else (
"speech_end" if transcript_event.final else "voice_activity"
)
provider_event: dict[str, object] = {
"type": event_type,
"provider": provider,
"source": "streaming_stt",
"start_ms": round(start * 1000),
"duration_ms": round(duration * 1000),
"is_final": transcript_event.final,
}
if transcript_event.confidence is not None:
provider_event["confidence"] = transcript_event.confidence
return provider_event
class DeepgramStreamingSession:
"""One Deepgram Listen WebSocket, scoped to exactly one learner utterance."""
@ -364,29 +387,21 @@ class DeepgramStreamingSession:
if not display_text and not speech_final:
return
event_type = "speech_final" if speech_final else (
"speech_end" if is_final else "voice_activity"
transcript_event = StreamingTranscriptEvent(
text=display_text,
final=is_final,
speech_final=speech_final,
confidence=confidence,
)
provider_event = _streaming_provider_event(
"deepgram",
transcript_event,
start=start,
duration=duration,
)
provider_event: dict[str, object] = {
"type": event_type,
"provider": "deepgram",
"source": "streaming_stt",
"start_ms": round(start * 1000),
"duration_ms": round(duration * 1000),
"is_final": is_final,
}
if confidence is not None:
provider_event["confidence"] = confidence
if is_final or speech_final:
self._provider_events.append(provider_event)
await self._on_event(
StreamingTranscriptEvent(
text=display_text,
final=is_final,
speech_final=speech_final,
confidence=confidence,
)
)
await self._on_event(transcript_event)
def _consume_final_words(self, value: object) -> None:
if not isinstance(value, list):
@ -620,29 +635,21 @@ class LocalWhisperStreamingSession:
if not display_text and not speech_final:
return
event_type = "speech_final" if speech_final else (
"speech_end" if is_final else "voice_activity"
transcript_event = StreamingTranscriptEvent(
text=display_text,
final=is_final,
speech_final=speech_final,
confidence=confidence,
)
provider_event = _streaming_provider_event(
"local_whisper",
transcript_event,
start=start,
duration=duration,
)
provider_event: dict[str, object] = {
"type": event_type,
"provider": "local_whisper",
"source": "streaming_stt",
"start_ms": round(start * 1000),
"duration_ms": round(duration * 1000),
"is_final": is_final,
}
if confidence is not None:
provider_event["confidence"] = confidence
if is_final or speech_final:
self._provider_events.append(provider_event)
await self._on_event(
StreamingTranscriptEvent(
text=display_text,
final=is_final,
speech_final=speech_final,
confidence=confidence,
)
)
await self._on_event(transcript_event)
def _consume_final_words(self, value: object, *, offset: float) -> None:
if not isinstance(value, list):