"""Pydantic v2 models for domain_policy.yaml. Loader 가 yaml → DomainPolicy 로 파싱. Schema 위반 시 ValidationError → 배포 차단. """ from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator # documents.category enum (migration 143 + 152) UICategory = Literal["document", "library", "news", "memo", "audio", "video", "law"] SelfDeclareSemantics = Literal["additive_trigger_only"] class SubjectDomain(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) description: str suggested_ui_category: UICategory high_impact: bool = False default_risk_flags: tuple[str, ...] = () deep_summary_risk_flags: tuple[str, ...] = () keywords: tuple[str, ...] = () note: str | None = None class FallbackDomain(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) name: str description: str suggested_ui_category: UICategory high_impact: bool = False default_risk_flags: tuple[str, ...] = () requires_human_review: bool = True class RiskFlag(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) description: str requires_26b: bool synthesis_directive: str | None = None output_mask_required: bool = False @field_validator("synthesis_directive") @classmethod def _directive_length(cls, v: str | None) -> str | None: if v is not None and len(v) > 500: raise ValueError("synthesis_directive must be <= 500 chars") return v class ForbiddenRule(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) id: str description: str applies_when_subject_in: tuple[str, ...] detection_patterns: tuple[str, ...] = () class Escalation(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) confidence_threshold: float = Field(ge=0.0, le=1.0) context_char_cap_4b: int = Field(gt=0) context_char_cap_26b: int = Field(gt=0) escalate_on_multi_doc_count: int = Field(ge=1) class HealthRange(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) min: float | None = None max: float | None = None class Observability(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) required_event_fields: tuple[str, ...] health_ranges: dict[str, HealthRange] class DomainPolicy(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) version: int last_updated: str scope: tuple[str, ...] self_declare_semantics: SelfDeclareSemantics subject_domains: dict[str, SubjectDomain] fallback_domain: FallbackDomain risk_flags: dict[str, RiskFlag] forbidden_for_4b: tuple[ForbiddenRule, ...] escalation: Escalation observability: Observability @model_validator(mode="after") def _cross_reference_check(self) -> "DomainPolicy": """Cross-field validation — yaml 내부 일관성.""" known_flags = set(self.risk_flags.keys()) # 1. 모든 subject_domain.default_risk_flags 가 risk_flags 에 정의돼 있어야 함 for name, dom in self.subject_domains.items(): for flag in (*dom.default_risk_flags, *dom.deep_summary_risk_flags): if flag not in known_flags: raise ValueError( f"subject_domain '{name}' references unknown risk_flag '{flag}'" ) for flag in self.fallback_domain.default_risk_flags: if flag not in known_flags: raise ValueError( f"fallback_domain references unknown risk_flag '{flag}'" ) # 2. forbidden_for_4b.applies_when_subject_in 의 도메인이 subject_domains 에 있어야 함 known_domains = set(self.subject_domains.keys()) for rule in self.forbidden_for_4b: for dom_name in rule.applies_when_subject_in: if dom_name not in known_domains: raise ValueError( f"forbidden rule '{rule.id}' references unknown subject_domain '{dom_name}'" ) return self