#!/usr/bin/env python3 """Validate a decision against block-schema rules. Usage: python validate-decision.py Checks: 1. Neutral background (block-vav) — no party quotes or value words 2. Weight compliance — blocks within expected ranges 3. Structural integrity — all required blocks present 4. Claims coverage — every claim in block-zayin addressed in block-yod """ import asyncio import json import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src")) from legal_mcp.services.db import get_pool, init_schema, close_pool # Value/judgment words that shouldn't appear in neutral background VALUE_WORDS = [ "חריג", "חטא", "בעייתי", "מזעזע", "שערורייתי", "מגוחך", "נפשע", "פגום", "חמור", "מקומם", "בלתי סביר", "מופרז", "מגונה", "פסול", "נלוז", "מטריד", ] # Party quote indicators QUOTE_INDICATORS = [ r"לטענת\s+(העוררי|המשיב|מבקשי)", r"לדברי\s+(העוררי|המשיב|מבקשי)", r"העורר\s+טוען", r"המשיבה\s+טוענת", r"לשיטת\s+(העוררי|המשיב)", ] # Expected weight ranges per block type (for רישוי appeals) WEIGHT_RANGES_LICENSING = { "block-he": (0.5, 5), "block-vav": (3, 40), "block-zayin": (13, 40), "block-chet": (0, 15), "block-tet": (0, 15), "block-yod": (30, 75), "block-yod-alef": (1, 10), "block-yod-bet": (0, 2), } # Expected weight ranges for היטל השבחה WEIGHT_RANGES_LEVY = { "block-he": (0, 5), "block-vav": (2, 20), "block-zayin": (15, 40), "block-chet": (0, 25), "block-tet": (0, 15), "block-yod": (25, 75), "block-yod-alef": (1, 10), "block-yod-bet": (0, 3), } def check_neutral_background(content: str) -> list[str]: """Check block-vav for neutrality violations.""" issues = [] if not content: return issues lines = content.split("\n") for i, line in enumerate(lines): # Check value words for word in VALUE_WORDS: if word in line: issues.append(f"מילת שיפוט ברקע (שורה {i+1}): \"{word}\" — \"{line[:80]}...\"") # Check party quotes for pattern in QUOTE_INDICATORS: if re.search(pattern, line): match = re.search(pattern, line).group() issues.append(f"ציטוט מצד ברקע (שורה {i+1}): \"{match}\" — \"{line[:80]}...\"") return issues def check_weight_compliance(blocks: list[dict], appeal_type: str) -> list[str]: """Check block weights are within expected ranges.""" issues = [] ranges = WEIGHT_RANGES_LEVY if appeal_type == "levy" else WEIGHT_RANGES_LICENSING total_words = sum(b["word_count"] for b in blocks) if total_words == 0: return ["אין תוכן בהחלטה"] for block in blocks: bid = block["block_id"] if bid in ranges and block["word_count"] > 0: weight = block["word_count"] / total_words * 100 low, high = ranges[bid] if weight < low: issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מתחת לטווח ({low}-{high}%)") elif weight > high: issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מעל לטווח ({low}-{high}%)") return issues def check_structural_integrity(blocks: list[dict]) -> list[str]: """Check all required blocks are present.""" issues = [] required = ["block-he", "block-zayin", "block-yod"] block_ids = {b["block_id"] for b in blocks if b["word_count"] > 0} for req in required: if req not in block_ids: issues.append(f"בלוק חובה חסר: {req}") # Check discussion is the heaviest block yod = next((b for b in blocks if b["block_id"] == "block-yod"), None) if yod: max_block = max((b for b in blocks if b["block_id"] not in ("block-alef", "block-bet", "block-gimel", "block-dalet")), key=lambda x: x["word_count"], default=None) if max_block and max_block["block_id"] != "block-yod": issues.append(f"בלוק הדיון (י) אינו הבלוק הגדול ביותר — {max_block['title']} ({max_block['word_count']} מילים) גדול יותר") return issues def check_no_duplication(vav_content: str, yod_content: str) -> list[str]: """Check block-yod doesn't repeat block-vav content.""" issues = [] if not vav_content or not yod_content: return issues # Find sentences from background that appear verbatim in discussion vav_sentences = [s.strip() for s in re.split(r'[.!?]', vav_content) if len(s.strip()) > 30] for sent in vav_sentences: if sent in yod_content: issues.append(f"כפילות: משפט מהרקע חוזר בדיון — \"{sent[:60]}...\"") return issues async def main(): if len(sys.argv) < 2: print("שימוש: python validate-decision.py <מספר_תיק>") sys.exit(1) case_number = sys.argv[1] await init_schema() pool = await get_pool() async with pool.acquire() as conn: case = await conn.fetchrow( "SELECT * FROM cases WHERE case_number = $1", case_number ) if not case: print(f"תיק {case_number} לא נמצא") sys.exit(1) decision = await conn.fetchrow( "SELECT * FROM decisions WHERE case_id = $1", case["id"], ) if not decision: print(f"אין החלטה לתיק {case_number}") sys.exit(1) blocks = await conn.fetch( """SELECT block_id, title, content, word_count, weight_percent FROM decision_blocks WHERE decision_id = $1 ORDER BY block_index""", decision["id"], ) blocks = [dict(b) for b in blocks] claims_count = await conn.fetchval( "SELECT count(*) FROM claims WHERE case_id = $1", case["id"] ) await close_pool() # Determine appeal type num = case_number.split("/")[0].split("+")[0].split("-")[0] if num.startswith("8"): appeal_type = "levy" appeal_type_heb = "היטל השבחה" elif num.startswith("9"): appeal_type = "compensation" appeal_type_heb = "פיצויים" else: appeal_type = "licensing" appeal_type_heb = "רישוי ובנייה" print(f"{'='*60}") print(f"ולידציה: {case_number} — {case['title']}") print(f"סוג: {appeal_type_heb} | מילים: {decision['total_words']} | טענות: {claims_count}") print(f"{'='*60}") all_issues = [] # 1. Neutral background vav = next((b for b in blocks if b["block_id"] == "block-vav"), None) issues = check_neutral_background(vav["content"] if vav else "") if issues: print(f"\n❌ רקע ניטרלי — {len(issues)} בעיות:") for i in issues: print(f" • {i}") all_issues.extend(issues) else: print("\n✅ רקע ניטרלי — תקין") # 2. Weight compliance issues = check_weight_compliance(blocks, appeal_type) if issues: print(f"\n⚠ משקלות — {len(issues)} חריגות:") for i in issues: print(f" • {i}") all_issues.extend(issues) else: print("\n✅ משקלות — בטווח") # 3. Structural integrity issues = check_structural_integrity(blocks) if issues: print(f"\n❌ מבנה — {len(issues)} בעיות:") for i in issues: print(f" • {i}") all_issues.extend(issues) else: print("\n✅ מבנה — תקין") # 4. No duplication yod = next((b for b in blocks if b["block_id"] == "block-yod"), None) issues = check_no_duplication( vav["content"] if vav else "", yod["content"] if yod else "", ) if issues: print(f"\n⚠ כפילויות — {len(issues)} נמצאו:") for i in issues: print(f" • {i}") all_issues.extend(issues) else: print("\n✅ ללא כפילויות — תקין") # Summary print(f"\n{'='*60}") if all_issues: print(f"סה\"כ: {len(all_issues)} בעיות נמצאו") else: print("✅ ההחלטה עומדת בכל הכללים") if __name__ == "__main__": asyncio.run(main())