세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터

- 누적 작업트리 커밋: 회기 평가 복구·durable 저장, 라이브 코치 이력/근거, 교수자 학생분석, 음성 비언어 메타, PII 마스킹, 운영 티켓/헬스 등
- 문서: 완료 기록 docs/archive/ 냉동 보관, docs/ 단일 인덱스(docs/README.md)+통합 TODO(docs/TODO.md)로 정리
- 리팩터(행위 보존): Stage enum SSOT(taxonomy 소유·state_machine re-export), store recent/masked_turns 중복 제거, speaker_ko_label 단일 헬퍼, _list_sessions N+1 제거(state/turns 배치 + 턴평가 하이드레이션 배치)
- 검증: 백엔드 pytest 352 passed, _list_sessions E2E chromium-single-run 2 passed
This commit is contained in:
Yun Chan 2026-07-02 02:50:36 +09:00
parent 7c41c3ce79
commit 778e8526d4
108 changed files with 6457 additions and 455 deletions

View file

@ -24,6 +24,7 @@ from app.services.phase3_kpi_contract import ( # noqa: E402
PHASE3_KPI_METRICS,
PREPOST_CSV_PATH,
)
from app.services.dataset_export import DATASET_ITEM_SCHEMA, scan_for_pii # noqa: E402
FORBIDDEN_HEADER_TERMS = {
@ -172,6 +173,31 @@ MANIFEST_KEYS = {
"source_window",
}
DATASET_ITEM_REQUIRED_KEYS = {
"client_states",
"feedback_scores",
"item_id",
"participant_key",
"persona_id",
"privacy",
"schema",
"session_key",
"source_refs",
"speaker",
"stage",
"supervisor_comments",
"techniques",
"text_masked",
"turn_key",
}
DATASET_ITEM_ARRAY_KEYS = {
"client_states",
"feedback_scores",
"supervisor_comments",
"techniques",
}
class Report:
def __init__(self) -> None:
@ -357,6 +383,128 @@ def numeric_value(value: Any) -> float | None:
return None
def _dataset_jsonl_issue(report: Report, approved: bool, message: str) -> None:
if approved:
report.error(message)
else:
report.warn(message)
def validate_dataset_jsonl(
path: Path,
rel_path: str,
report: Report,
*,
expected_rows: Any,
approved: bool,
) -> None:
row_count = 0
try:
with path.open("r", encoding="utf-8", newline="\n") as handle:
for line_number, raw_line in enumerate(handle, start=1):
line = raw_line.strip()
if not line:
continue
row_count += 1
try:
record = json.loads(line)
except json.JSONDecodeError as exc:
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: invalid JSONL record: {exc.msg}",
)
continue
if not isinstance(record, dict):
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: dataset item must be a JSON object",
)
continue
missing = sorted(DATASET_ITEM_REQUIRED_KEYS - set(record))
if missing:
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: dataset item missing keys: {', '.join(missing)}",
)
if record.get("schema") != DATASET_ITEM_SCHEMA:
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: dataset item schema must be {DATASET_ITEM_SCHEMA}",
)
if not isinstance(record.get("text_masked"), str) or not record.get("text_masked", "").strip():
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: text_masked must be a non-empty string",
)
for key in DATASET_ITEM_ARRAY_KEYS:
if key in record and not isinstance(record[key], list):
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: {key} must be an array",
)
source_refs = record.get("source_refs")
if source_refs is not None and not isinstance(source_refs, dict):
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: source_refs must be an object",
)
privacy = record.get("privacy")
if not isinstance(privacy, dict):
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: privacy must be an object",
)
else:
if privacy.get("direct_identifiers_removed") is not True:
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: privacy.direct_identifiers_removed must be true",
)
if not privacy.get("pii_scan_status"):
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: privacy.pii_scan_status is required",
)
if privacy.get("consent_scope") != "recursive_learning_seed":
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: privacy.consent_scope must be recursive_learning_seed",
)
findings = scan_for_pii(record)
if findings:
kinds = ", ".join(sorted({str(finding.get("kind")) for finding in findings}))
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}:{line_number}: dataset item contains blocked identifier evidence: {kinds}",
)
except UnicodeDecodeError:
_dataset_jsonl_issue(report, approved, f"{rel_path}: not valid UTF-8")
return
except OSError as exc:
_dataset_jsonl_issue(report, approved, f"{rel_path}: cannot read dataset JSONL: {exc}")
return
if isinstance(expected_rows, int) and row_count != expected_rows:
_dataset_jsonl_issue(
report,
approved,
f"{rel_path}: rows mismatch for {path.name}: manifest={expected_rows} actual={row_count}",
)
def validate_manifest_file_entry(
root: Path,
rel_path: str,
@ -386,18 +534,33 @@ def validate_manifest_file_entry(
except ValueError:
report.error(f"{rel_path}: file path escapes evidence root: {file_path}")
return
if not approved:
return
if not candidate.exists():
report.error(f"{rel_path}: approved file missing: {file_path}")
if approved:
report.error(f"{rel_path}: approved file missing: {file_path}")
else:
report.warn(f"{rel_path}: file listed but missing: {file_path}")
return
if not candidate.is_file():
report.error(f"{rel_path}: approved file path is not a file: {file_path}")
if approved:
report.error(f"{rel_path}: approved file path is not a file: {file_path}")
else:
report.warn(f"{rel_path}: file path is not a file: {file_path}")
return
if isinstance(expected_sha, str) and expected_sha:
actual_sha = sha256_file(candidate)
if actual_sha != expected_sha:
report.error(f"{rel_path}: sha256 mismatch for {file_path}")
if approved:
report.error(f"{rel_path}: sha256 mismatch for {file_path}")
else:
report.warn(f"{rel_path}: sha256 mismatch for {file_path}")
if item.get("schema") == DATASET_ITEM_SCHEMA or str(file_path).endswith(".jsonl"):
validate_dataset_jsonl(
candidate,
rel_path,
report,
expected_rows=item.get("rows"),
approved=True,
)
def validate_manifest(root: Path, report: Report) -> None: