Files
legal-ai/scripts/validate-decision.py
Chaim d5ccf03e4c Add docs, scripts, skills, commands, and taskmaster config to repo
Includes:
- docs/: architecture, block-schema, migration-plan, product-specification
- scripts/: bidi_table, decompose-decisions, extract-claims, seed-knowledge, etc.
- skill-legal-decision/: SKILL.md + references + block-schema
- skill-legal-assistant/: SKILL.md
- skill-legal-docx/: SKILL.md + references
- .claude/commands/: bidi-table skill
- .taskmaster/: task config + PRDs
- .gitignore: exclude legacy/, kiryat-yearim/, node_modules/, memory/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 14:19:17 +00:00

258 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Validate a decision against block-schema rules.
Usage: python validate-decision.py <case_number>
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())