"""Structure verifier (LRLM-style). Detects deontic structure cues in free text, compares source ⊕ output, and raises HIGH-risk alerts when the output silently removes exceptions or flips obligations ↔ permissions. """ from __future__ import annotations from typing import List from ..core.types import ( Query, Retrieved, StructureSignature, VerificationAlert, VerificationResult, ) # Lightweight deontic lexicon (Hebrew + English). CUES = { "OBL": ["חייב", "מחויב", "נדרש", "must", "shall", "required"], "PERM": ["רשאי", "יכול", "may", "allowed", "permitted"], "PROHIB": ["אסור", "לא יעשה", "shall not", "may not", "prohibited", "forbidden"], "COND": ["אם", "במקרה", "בתנאי", "if", "when", "provided that"], "EXC": ["למעט", "פרט ל", "חריג", "except", "unless", "save that"], "REF": ["סעיף", "חוק ", "section", "article", "§"], } def signature_of_text(text: str) -> StructureSignature: lo = (text or "").lower() sig = StructureSignature() for k, cues in CUES.items(): if any(c in lo or c in text for c in cues): setattr(sig, k, True) return sig class StructureVerifier: """Compare answer signature to union of source signatures.""" name = "structure" def verify_answer( self, answer: str, sources: List[Retrieved], ) -> VerificationResult: if not answer or not sources: return VerificationResult( passed=True, alerts=[], citation_coverage=0.0, faithfulness=0.0, structure_match=True, ) ans_sig = signature_of_text(answer) src_union = StructureSignature() for r in sources: s = signature_of_text(r.chunk.text) for k in s.to_dict().keys(): if getattr(s, k): setattr(src_union, k, True) alerts: List[VerificationAlert] = [] # HIGH-risk changes if src_union.OBL and ans_sig.PERM and not ans_sig.OBL: alerts.append(VerificationAlert( type="deontic_flip_OBL_to_PERM", risk="HIGH", impact="Obligation was silently weakened to permission.", )) if src_union.PROHIB and not ans_sig.PROHIB: alerts.append(VerificationAlert( type="prohibition_dropped", risk="HIGH", impact="Source prohibition is missing from the answer.", )) if src_union.EXC and not ans_sig.EXC: alerts.append(VerificationAlert( type="exception_removed", risk="HIGH", impact="Source contained an exception that is absent in the answer.", )) # MEDIUM if src_union.COND and not ans_sig.COND: alerts.append(VerificationAlert( type="condition_dropped", risk="MEDIUM", impact="A conditional qualifier is missing from the answer.", )) passed = not any(a.risk == "HIGH" for a in alerts) return VerificationResult( passed=passed, alerts=alerts, citation_coverage=0.0, faithfulness=0.0, structure_match=passed, ) # Match the Verifier Protocol. def verify( self, query: Query, answer: str, context: List[Retrieved], ) -> VerificationResult: return self.verify_answer(answer, context)