"""Domain ports for compliance history persistence.""" from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime from typing import Optional @dataclass class FindingRecord: """Single finding row linked to an analysis.""" id: str analysis_id: str seq: int title: str description: str status: str # "ok" | "warn" | "risk" clause_ref: Optional[str] = None @dataclass class AnalysisRecord: """Full compliance analysis record with nested findings.""" id: str # UUID string; empty string means not yet persisted created_at: datetime created_by: Optional[str] doc_name: str standard_name: str risk_score: int conclusion: str actions: list # list[dict] — serialised action items para_text: str highlight_terms: list # list[str] findings: list[FindingRecord] = field(default_factory=list) class ComplianceRepository(ABC): """Port for persisting and retrieving compliance analysis records.""" @abstractmethod def save_analysis(self, record: AnalysisRecord) -> str: """Persist a new analysis record and return the assigned UUID string.""" @abstractmethod def list_analyses(self, limit: int = 50, offset: int = 0) -> list[AnalysisRecord]: """Return analyses ordered by created_at DESC, without nested findings.""" @abstractmethod def get_analysis(self, analysis_id: str) -> Optional[AnalysisRecord]: """Return a single analysis with all nested findings, or None.""" @abstractmethod def delete_analysis(self, analysis_id: str) -> None: """Delete an analysis and all related findings and chat messages (cascade).""" @abstractmethod def save_message(self, analysis_id: str, finding_id: str, role: str, content: str) -> str: """Persist a chat message and return its UUID string.""" @abstractmethod def get_messages(self, finding_id: str) -> list[dict]: """Return chat messages for a finding ordered by created_at ASC. Each dict has keys: id, role, content, created_at (ISO string). """