세션 평가·라이브코치·교수자 분석 라운드 마감 + 문서 정리 + 코드품질 리팩터
- 누적 작업트리 커밋: 회기 평가 복구·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:
parent
7c41c3ce79
commit
778e8526d4
108 changed files with 6457 additions and 455 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue